通过延迟/性能测量确定NUMA布局

最近我一直在观察内存密集型工作负载中的性能影响,我无法解释。 试图找到底部我开始运行几个微基准测试,以确定常见的性能参数,如缓存行大小和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循环,对于非常小的arrays大小甚至100,000次。 然后,在我的IvyBridge笔记本上,我可以清楚地看到L1结束于64K,L2处于256K,甚至L3处于6M。

现在谈谈我的真正问题:在NUMA系统中,任何一个核心都将获得远程主内存甚至共享缓存,这些缓存不一定与其本地缓存和内存一样接近。 我希望看到延迟/性能的差异,从而确定在保持快速缓存/部分内存时我可以分配多少内存。

为此,我改进了我的测试,以1/10 MB块的forms遍历内存,分别测量延迟,然后收集最快的块,大致如下:

 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(因此我的L1缓存为~32K),256K / 512K(因此我的L2缓存大小为~256K)和6144K / 8192K(因此我的L3缓存大小为~6M)之间存在明显的步骤。