c语言 - 通过延迟/性能测量确定 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开始,我看到每个元素的时间没有进一步增加,即每次迭代都需要获取一个新的缓存行来主导运行时。 从 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)之间有明显的步骤。

最新更新