最近我一直在观察内存密集型工作负载的性能影响,我无法解释。为了深入了解这一点,我开始运行几个微基准测试,以确定常见的性能参数,如缓存行大小和 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
开始,我看到每个元素的时间没有进一步增加,即每次迭代都需要获取一个新的缓存行来主导运行时。 从 1K 到 16384K 不等ARRAY_SIZE
保持STEP=64
我能够创建一个漂亮的图,展示大致对应于 L1、L2 和 L3 延迟的步进模式。但是,对于非常小的数组大小,甚至 100,000 次,有必要重复 for 循环多次,以获得可靠的数字。然后,在我的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 延迟?如果是,我做错了什么? 我禁用地址空间随机化只是为了确保并排除奇怪的缓存别名效果。在测量之前,是否还有其他操作系统方面必须进行调整?
是否有可能观察到这样的 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(所以我的一级缓存是 ~32K)、256K/512K(所以我的二级缓存大小是 ~256K)和 6144K/8192K(所以我的三级缓存大小是 ~6M)之间有明显的步骤。