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

4

最近我观察到一些内存密集型工作负载的性能问题,但无法解释。为了找出问题所在,我开始运行几个微基准测试,以确定常见的性能参数,如缓存线大小和 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:
[..]

STEP从1变化到128,结果显示从STEP=64开始,每个元素的时间不再进一步增加,即每次迭代需要获取一个新的高速缓存行来支配运行时间。
ARRAY_SIZE从1K变化到16384K,保持STEP=64,我能够创建一个漂亮的图表,展示出大致对应于L1、L2和L3延迟的步进模式。然而,为了得到可靠的数据,对于非常小的数组大小,甚至需要重复循环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延迟?如果是,我做错了什么?

我禁用了地址空间随机化,以确保排除奇怪的缓存别名效应。在测量之前,是否还有其他操作系统方面的调整需要进行?


1
关于预取器,英特尔的预取器执行黑魔法。我听说它会应用多项式拟合来神奇地预取当前步幅等。你最好使用MTRRs将一段内存标记为不可缓存,然后在该范围内进行基准测试。 - lockcmpxchg8b
1个回答

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)。


啊,我曾考虑过使用单独的随机数组来干扰预取器,但是我认为它本身就很热,会污染我的缓存。使用随机值初始化数组并进行指针追踪是个好主意。我会尝试一下,谢谢。至于NUMA控制 - 我实际上在一个不是NUMA的系统上,但仍表现出这样的特性(malloc不具备NUMA感知能力),因此我不能依赖numactl。即使不绑定,当数组足够大时,应该可以强制从不同节点中获取内存,对吗? - Sonny O'Rullivan
@SonnyO'Rullivan如果你的系统不是NUMA,那么这取决于这些页面如何交错。你可能会从NUMA0中获取一个页面,然后从NUMA1中获取几个页面。那么这样的方法确实只会给你带来噪音。你可以尝试另一种方法,通过使用相同的数据污染缓存,每次只交换一个页面。但是可能很难检测到这样微小的变化...有一件事是确定的:你应该将你的测试与页面大小对齐... - Andriy Berestovskyy
@AndriyBerestovskyy - 能否给我你写的基准测试代码?那会对我很有帮助。 - Mayank Jain

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接