【问题标题】:Determine NUMA layout via latency/performance measurements通过延迟/性能测量确定 NUMA 布局
【发布时间】:2018-05-24 19:08:43
【问题描述】:

最近我一直在观察无法解释的内存密集型工作负载的性能影响。为了弄清楚这一点,我开始运行几个微基准测试以确定常见的性能参数,如缓存行大小和 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 延迟的步进模式。但是,为了获得可靠的数字,必须多次重复 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 延迟?如果是,我做错了什么? 我禁用地址空间随机化只是为了确定并排除奇怪的缓存别名效应。在测量之前是否需要对操作系统方面的其他方面进行调整?

【问题讨论】:

  • Re:预取器,英特尔预取器执行黑魔法。我听说它应用多个多项式拟合来神奇地以您当前的步幅预取,等等。您最好的选择可能是使用 MTRR 来标记不可缓存的内存范围,然后在该范围内进行基准测试。跨度>

标签: c performance caching prefetch numa


【解决方案1】:

是否有可能观察到这样的 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。只要数组足够大,即使没有绑定它,是否应该强制从不同的节点获取内存?
  • @SonnyO'Rullivan 如果您的系统不是 NUMA,那么这真的取决于这些页面是如何交错的。您可能会从 NUMA0 获得一页,而从 NUMA1 获得接下来的几页。那么这种方法真的会给你带来噪音。您可以尝试另一种方法,通过使用相同的数据污染缓存并一次只交换一个页面。但是可能很难检测到这种细微的变化......有一件事是肯定的:您应该将测试与页面大小对齐......
  • @AndriyBerestovskyy - 我可以得到你写的基准测试代码吗?它会帮我很多。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-05-03
  • 1970-01-01
  • 1970-01-01
  • 2021-01-02
  • 1970-01-01
相关资源
最近更新 更多