最近我一直在观察内存密集型工作负载中的性能影响,我无法解释。试图找到底部我开始运行几个微基准测试,以确定常见的性能参数,如缓存行大小和L1 / L2 / L3缓存大小(我已经知道它们,我只是想看看我的测量是否反映了实际值)。
对于缓存行测试,我的代码大致如下所示(Linux C,但当然这个概念类似于Windows等):
char *array = malloc (ARRAY_SIZE);
int count = ARRAY_SIZE / STEP;
clock_gettime(CLOCK_REALTIME, &start_time);
for (int i = 0; i < ARRAY_SIZE; i += STEP) {
array[i]++;
}
clock_gettime(CLOCK_REALTIME, &end_time);
// calculate time per element here:
[..]
从1到128改变STEP
表明从STEP=64
开始,我看到每个元素的时间没有进一步增加,即每次迭代都需要获取一个主导运行时的新缓存行。
改变ARRAY_SIZE
从1K到16384K保持STEP=64
我能够创建一个很好的绘图,展示一个大致对应于L1,L2和L3延迟的步骤模式。为了得到可靠的数字,有必要多次重复for循环,对于非常小的阵列大小甚至100,000次。然后,在我的IvyBridge笔记本上,我可以清楚地看到L1结束于64K,L2处于256K,甚至L3处于6M。
现在谈谈我的真正问题:在NUMA系统中,任何一个核心都将获得远程主内存,甚至是共享缓存,它不一定与本地缓存和内存一样接近。我希望看到延迟/性能的差异,从而确定在保持快速缓存/部分内存时我可以分配多少内存。
为此,我对我的测试进行了改进,以1/10 MB块的形式遍历内存,分别测量延迟,然后收集最快的块,大致如下:
for (int chunk_start = 0; chunk_start < ARRAY_SIZE; chunk_start += CHUNK_SIZE) {
int chunk_end = MIN (ARRAY_SIZE, chunk_start + CHUNK_SIZE);
int chunk_els = CHUNK_SIZE / STEP;
for (int i = chunk_start; i < chunk_end; i+= STEP) {
array[i]++;
}
// calculate time per element
[..]
一旦我开始将ARRAY_SIZE
增加到大于L3大小的东西,我就会得到一些不可思议的数字,即使大量的重复也无法实现。我无法通过这种方式确定可用于性能评估的模式,更不用说确定NUMA条带的确切位置的开始,结束或位置。
然后,我认为硬件预取器非常智能,能够识别我的简单访问模式,并在访问之前简单地将所需的行提取到缓存中。向数组索引添加一个随机数会增加每个元素的时间,但似乎没有多大帮助,可能是因为我每次迭代都有rand ()
次调用。预先计算一些随机值并将它们存储在一个数组中对我来说似乎不是一个好主意,因为这个数组也会存储在热缓存中并使我的测量值偏斜。将STEP
增加到4097或8193也没有多大帮助,预取者必须比我聪明。
我的方法是否合理/可行还是我错过了更大的图片?是否可以观察到这样的NUMA延迟?如果是的话,我做错了什么? 我禁用地址空间随机化只是为了确保并排除奇怪的缓存别名效应。在测量之前是否还有其他必须调整的操作系统呢?
答案 0 :(得分:3)
是否可以观察到这样的NUMA延迟?如果是的话,我做错了什么?
内存分配器可识别NUMA,因此默认情况下,在明确要求在另一个节点上分配内存之前,您不会观察到任何NUMA效果。实现这种效果的最简单方法是numactl(8)。只需在一个节点上运行您的应用程序并将内存分配绑定到另一个节点,如下所示:
numactl --cpunodebind 0 --membind 1 ./my-benchmark
另见numa_alloc_onnode(3)。
在测量之前是否还需要调整其他操作系统?
关闭CPU缩放,否则测量可能会有噪音:
find '/sys/devices/system/cpu/' -name 'scaling_governor' | while read F; do
echo "==> ${F}"
echo "performance" | sudo tee "${F}" > /dev/null
done
现在关于测试本身。当然,要测量延迟,访问模式必须是(伪)随机的。否则,您的测量结果将受到快速缓存命中的影响。
以下是如何实现这一目标的示例:
使用随机数填充数组:
static void random_data_init()
{
for (size_t i = 0; i < ARR_SZ; i++) {
arr[i] = rand();
}
}
每次基准迭代执行1M运算操作以降低测量噪声。使用数组随机数跳过几个缓存行:
const size_t OPERATIONS = 1 * 1000 * 1000; // 1M operations per iteration
int random_step_sizeK(size_t size)
{
size_t idx = 0;
for (size_t i = 0; i < OPERATIONS; i++) {
arr[idx & (size - 1)]++;
idx += arr[idx & (size - 1)] * 64; // assuming cache line is 64B
}
return 0;
}
以下是i5-4460 CPU @ 3.20GHz的结果:
----------------------------------------------------------------
Benchmark Time CPU Iterations
----------------------------------------------------------------
random_step_sizeK/4 4217004 ns 4216880 ns 166
random_step_sizeK/8 4146458 ns 4146227 ns 168
random_step_sizeK/16 4188168 ns 4187700 ns 168
random_step_sizeK/32 4180545 ns 4179946 ns 163
random_step_sizeK/64 5420788 ns 5420140 ns 129
random_step_sizeK/128 6187776 ns 6187337 ns 112
random_step_sizeK/256 7856840 ns 7856549 ns 89
random_step_sizeK/512 11311684 ns 11311258 ns 57
random_step_sizeK/1024 13634351 ns 13633856 ns 51
random_step_sizeK/2048 16922005 ns 16921141 ns 48
random_step_sizeK/4096 15263547 ns 15260469 ns 41
random_step_sizeK/6144 15262491 ns 15260913 ns 46
random_step_sizeK/8192 45484456 ns 45482016 ns 23
random_step_sizeK/16384 54070435 ns 54064053 ns 14
random_step_sizeK/32768 59277722 ns 59273523 ns 11
random_step_sizeK/65536 63676848 ns 63674236 ns 10
random_step_sizeK/131072 66383037 ns 66380687 ns 11
32K / 64K之间有明显的步骤(所以我的L1缓存大约是32K),256K / 512K(所以我的L2缓存大小是~256K)和6144K / 8192K(所以我的L3缓存大小是~6M)。 / p>