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

时间:2017-12-11 09:36:53

标签: c performance caching prefetch 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循环,对于非常小的阵列大小甚至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 个答案:

答案 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>