【问题标题】:Copying 64 bytes of memory with NT stores to one full cache line vs. 2 consecutive partial cache lines使用 NT 存储将 64 字节内存复制到一个完整的高速缓存行与 2 个连续的部分高速缓存行
【发布时间】:2020-05-05 03:39:27
【问题描述】:

我正在阅读有关写入组合内存的英特尔优化手册并编写了基准测试以了解它的工作原理。以下是我运行基准测试的 2 个函数:

memcopy.h:

void avx_ntcopy_cache_line(void *dest, const void *src);

void avx_ntcopy_64_two_cache_lines(void *dest, const void *src);

memcopy.S:

avx_ntcopy_cache_line:
    vmovdqa ymm0, [rdi]
    vmovdqa ymm1, [rdi + 0x20]
    vmovntdq [rsi], ymm0
    vmovntdq [rsi + 0x20], ymm1
    ;intentionally no sfence after nt-store
    ret

avx_ntcopy_64_two_cache_lines:
    vmovdqa ymm0, [rdi]
    vmovdqa ymm1, [rdi + 0x40]
    vmovntdq [rsi], ymm0
    vmovntdq [rsi + 0x40], ymm1
    ;intentionally no sfence after nt-store
    ret

基准测试的主要功能如下所示:

#include <stdlib.h>
#include <inttypes.h>
#include <x86intrin.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include "memcopy.h"

#define ITERATIONS 1000000

//As @HadiBrais noted, there might be an issue with 4K aliasing
_Alignas(64) char src[128];
_Alignas(64) char dest[128];

static void run_benchmark(unsigned runs, unsigned run_iterations,
                    void (*fn)(void *, const void*), void *dest, const void* src);

int main(void){
    int fd = open("/dev/urandom", O_RDONLY);
    read(fd, src, sizeof src);

    run_benchmark(20, ITERATIONS, avx_ntcopy_cache_line, dest, src);
    run_benchmark(20, ITERATIONS, avx_ntcopy_64_two_cache_lines, dest, src);
}

static int uint64_compare(const void *u1, const void *u2){
    uint64_t uint1 = *(uint64_t *) u1;
    uint64_t uint2 = *(uint64_t *) u2;
    if(uint1 < uint2){
        return -1;
    } else if (uint1 == uint2){
        return 0;
    } else {
        return 1;
    }
}

static inline uint64_t benchmark_2cache_lines_copy_function(unsigned iterations, void (*fn)(void *, const void *),
                                               void *restrict dest, const void *restrict src){
    uint64_t *results = malloc(iterations * sizeof(uint64_t));
    unsigned idx = iterations;
    while(idx --> 0){
        uint64_t start = __rdpmc((1<<30)+1);
        fn(dest, src);
        fn(dest, src);
        fn(dest, src);
        fn(dest, src);
        fn(dest, src);
        fn(dest, src);
        fn(dest, src);
        fn(dest, src);
        fn(dest, src);
        fn(dest, src);
        fn(dest, src);
        fn(dest, src);
        fn(dest, src);
        fn(dest, src);
        fn(dest, src);
        fn(dest, src);
        uint64_t finish = __rdpmc((1<<30)+1);
        results[idx] = (finish - start) >> 4;
    }
    qsort(results, iterations, sizeof *results, uint64_compare);
    //median
    return results[iterations >> 1];
}

static void run_benchmark(unsigned runs, unsigned run_iterations,
                    void (*fn)(void *, const void*), void *dest, const void* src){
    unsigned current_run = 1;
    while(current_run <= runs){
        uint64_t time = benchmark_2cache_lines_copy_function(run_iterations, fn, dest, src);
        printf("Run %d result: %lu\n", current_run, time);
        current_run++;
    }
}

使用选项编译

-Werror \
-Wextra
-Wall \
-pedantic \
-Wno-stack-protector \
-g3 \
-O3 \
-Wno-unused-result \
-Wno-unused-parameter

运行基准测试我得到了以下结果:

我。 avx_ntcopy_cache_line:

Run 1 result: 61
Run 2 result: 61
Run 3 result: 61
Run 4 result: 61
Run 5 result: 61
Run 6 result: 61
Run 7 result: 61
Run 8 result: 61
Run 9 result: 61
Run 10 result: 61
Run 11 result: 61
Run 12 result: 61
Run 13 result: 61
Run 14 result: 61
Run 15 result: 61
Run 16 result: 61
Run 17 result: 61
Run 18 result: 61
Run 19 result: 61
Run 20 result: 61

perf:

 Performance counter stats for './bin':

     3 503 775 289      L1-dcache-loads                                               (18,87%)
        91 965 805      L1-dcache-load-misses     #    2,62% of all L1-dcache hits    (18,94%)
     2 041 496 256      L1-dcache-stores                                              (19,01%)
         5 461 440      LLC-loads                                                     (19,08%)
         1 108 179      LLC-load-misses           #   20,29% of all LL-cache hits     (19,10%)
        18 028 817      LLC-stores                                                    (9,55%)
       116 865 915      l2_rqsts.all_pf                                               (14,32%)
                 0      sw_prefetch_access.t1_t2                                      (19,10%)
           666 096      l2_lines_out.useless_hwpf                                     (19,10%)
        47 701 696      l2_rqsts.pf_hit                                               (19,10%)
        62 556 656      l2_rqsts.pf_miss                                              (19,10%)
         4 568 231      load_hit_pre.sw_pf                                            (19,10%)
        17 113 190      l2_rqsts.rfo_hit                                              (19,10%)
        15 248 685      l2_rqsts.rfo_miss                                             (19,10%)
        54 460 370      LD_BLOCKS_PARTIAL.ADDRESS_ALIAS                                     (19,10%)
    18 469 040 693      uops_retired.stall_cycles                                     (19,10%)
    16 796 868 661      uops_executed.stall_cycles                                     (19,10%)
    18 315 632 129      uops_issued.stall_cycles                                      (19,05%)
    16 176 115 539      resource_stalls.sb                                            (18,98%)
    16 424 440 816      resource_stalls.any                                           (18,92%)
    22 692 338 882      cycles                                                        (18,85%)

       5,780512545 seconds time elapsed

       5,740239000 seconds user
       0,040001000 seconds sys

二. avx_ntcopy_64_two_cache_lines:

Run 1 result: 6
Run 2 result: 6
Run 3 result: 6
Run 4 result: 6
Run 5 result: 6
Run 6 result: 6
Run 7 result: 6
Run 8 result: 6
Run 9 result: 6
Run 10 result: 6
Run 11 result: 6
Run 12 result: 6
Run 13 result: 6
Run 14 result: 6
Run 15 result: 6
Run 16 result: 6
Run 17 result: 6
Run 18 result: 6
Run 19 result: 6
Run 20 result: 6

perf:

 Performance counter stats for './bin':

     3 095 792 486      L1-dcache-loads                                               (19,26%)
        82 194 718      L1-dcache-load-misses     #    2,66% of all L1-dcache hits    (18,99%)
     1 793 291 250      L1-dcache-stores                                              (19,00%)
         4 612 503      LLC-loads                                                     (19,01%)
           975 438      LLC-load-misses           #   21,15% of all LL-cache hits     (18,94%)
        15 707 916      LLC-stores                                                    (9,47%)
        97 928 734      l2_rqsts.all_pf                                               (14,20%)
                 0      sw_prefetch_access.t1_t2                                      (19,21%)
           532 203      l2_lines_out.useless_hwpf                                     (19,19%)
        35 394 752      l2_rqsts.pf_hit                                               (19,20%)
        56 303 030      l2_rqsts.pf_miss                                              (19,20%)
         6 197 253      load_hit_pre.sw_pf                                            (18,93%)
        13 458 517      l2_rqsts.rfo_hit                                              (18,94%)
        14 031 767      l2_rqsts.rfo_miss                                             (18,93%)
        36 406 273      LD_BLOCKS_PARTIAL.ADDRESS_ALIAS                                     (18,94%)
     2 213 339 719      uops_retired.stall_cycles                                     (18,93%)
     1 225 185 268      uops_executed.stall_cycles                                     (18,94%)
     1 943 649 682      uops_issued.stall_cycles                                      (18,94%)
       126 401 004      resource_stalls.sb                                            (19,20%)
       202 537 285      resource_stalls.any                                           (19,20%)
     5 676 443 982      cycles                                                        (19,18%)

       1,521271014 seconds time elapsed

       1,483660000 seconds user
       0,032253000 seconds sys

可以看出,测量结果相差10倍。


我的解释

Intel Optimization Manual/3.6.9中所述:

对同一缓存行的不同部分的写入可以分组为 单个、全缓存线总线事务,而不是跨越 总线(因为它们没有被缓存)作为几个部分写入

我假设在 avx_ntcopy_cache_line 的情况下,我们有完整的 64 字节写入启动总线事务以将它们写出,这会禁止 rdtsc 乱序执行。

相比之下,在avx_ntcopy_64_two_cache_lines 的情况下,我们将 32 个字节写入不同的高速缓存行到 WC 缓冲区,并且没有触发总线事务。这允许rdtsc 乱序执行。

这种解释看起来非常可疑,与bus-cycles的区别不符:

avx_ntcopy_cache_line: 131 454 700

avx_ntcopy_64_two_cache_lines: 31 957 050

问题:造成这种测量差异的真正原因是什么?

【问题讨论】:

  • 你的解释对我来说毫无意义; bus-cycles 只是未停止的参考周期。 avx_ntcopy_cache_line 也可能遭受 4K 混叠(超过 avx_ntcopy_64_two_cache_lines?)。为两者测量LD_BLOCKS_PARTIAL.ADDRESS_ALIAS。 TSC的频率是多少?核心频率是固定的吗?
  • 所以摆脱 4K 锯齿;以后的加载不应与以前的商店对齐。为了您所关心的所有人的爱,请停止显示 TSC 周期计数(您在上一个问题中也这样做了)!如果不提供有关 TSC 和核心频率的信息,我应该如何处理这些数字?将它们转换为核心频率!
  • @HadiBrais 有一些 4K 混叠让我感到惊讶。这两个测试不是一遍又一遍地从同一对(非 4K 对齐)地址加载和存储吗?
  • @MargaretBloom:原始代码确实有_Alignas(4096),因此 dst 和 src 都是 4k 对齐的。一次迭代结束时的存储将与下一次迭代中的负载别名(因为正如您所说,指针没有递增)。代码现在已更改为 _Alignas(32),因此我们甚至不知道相对于缓存行的对齐方式,并且avx_ntcopy_cache_line 可能会或可能不会复制 2 个单独缓存行的相邻部分。
  • 当您将代码从 alignas(4096) 更改为 alignas(32) 时,您只更改了 perf 输出中的 1 行。这听起来完全令人难以置信,其他一切都是相同的,包括总时间。 (为什么你的 perf 输出不包括核心时钟周期?bus-cycles 只是 ref 周期和经过时间的冗余。)

标签: c performance assembly x86 avx


【解决方案1】:

假设:(完全)重叠存储到尚未刷新的 WC 缓冲区可以合并到其中。完成一条线会触发立即刷新,并且所有这些存储都离开核心很慢。

您报告的全行版本resource_stalls.sb 比 2 部分行版本多 100 倍。这和这个解释是一致的。

如果 2_lines 可以将 NT 存储提交到现有的 WC 缓冲区 (LFB),则存储缓冲区可以跟上存储指令的执行速度,通常会成为其他方面的瓶颈。 (可能只是前端,考虑到每对加载/存储的调用/ret 开销。当然call 确实包括一个存储。)您的perf 结果显示 18 亿存储(到 L1)超过 57 亿循环,在 1 个存储/循环的限制之内,我们可能期望存储在 WC 缓冲区中命中。

但是如果 WC 缓冲区被刷新,这发生在一行被完全写入时,它必须离开核心(这很慢),占用 LFB 一段时间,所以它不能用于提交以后的 NT 存储。当存储无法离开存储缓冲区时,它会被填满,并且核心停止为新的存储指令分配资源以进入后端。 (特别是问题/重命名/分配阶段停顿。)

您可能会更清楚地看到这种效果,任何 L2、L3、SQ、offcore req/resp 事件都会在 L1 之外接收所有这些流量。您包括了一些 L2 柜台,但那些可能不会拾取通过 L2 的 NT 商店。


Enhanced REP MOVSB for memcpy 建议 NT 存储需要更长的时间让 LFB “移交”到内存层次结构的外层,从而在请求开始其旅程后保持 LFB 被占用很长时间。 (也许是为了确保核心始终可以重新加载它刚刚存储的内容,否则不会丢失对运行中 NT 存储的跟踪以保持与 MESI 的一致性。)后来的sfence 还需要知道早期的 NT 存储何时可见到其他核心,所以我们不能在此之前的任何时候让它们不可见。

即使不是这样,对于所有这些 NT 存储请求,在某处仍会出现吞吐量瓶颈。所以另一种可能的机制是它们填满了一些缓冲区,然后核心不能再传递 LFB,所以它用完了 LFB 来提交 NT 存储,然后 SB 填充停滞分配。

一旦到达内存控制器,它们可能会合并,而不需要通过实际的外部内存总线进行突发传输,但是从内核到非内核到内存控制器的路径并不短。


即使对每 32 个存储执行 2x rdpmc 也不会减慢 CPU 速度以防止存储缓冲区被填满;您所看到的取决于在相对紧凑的循环中运行它,而不是从空存储缓冲区开始的一次性执行。此外,您对rdpmcrdtsc 的建议不会被重新排序。 WC 缓冲区刷新零意义。商店的执行不是按顺序排列的。执行rdtsc

TL:DR:您的rdpmc 为单个商店组计时并没有帮助,如果有任何东西通过减慢不会成为商店缓冲区瓶颈的快速案例来隐藏一些性能差异.

【讨论】:

  • 我一定是在这个问题出现前一分钟开始写我的答案,而手机并没有告诉你另一个答案出现了......无论如何,它们的想法似乎相同。
  • @BeeOnRope:呵呵。我想知道为什么您发布另一个答案而不是评论您确定这就是重复 NT 商店到同一条开放线路的方式。我不是 100% 确定,我不记得上次出现这个问题时的答案是什么,如果它曾经在 SO 上或在我自己的测试中。
  • 我最不确定的一件事是,如果同一行完全用 NT 存储编写,然后再一次,背靠背,真的会导致两次刷新一直到内存。至少在某些时候,第二个请求可能赶上第一个请求似乎是合理的,例如在外部缓存级别。也许这确实发生在这里,但当然除非发生很多,否则性能仍然会很糟糕。看看核心请求事件是否与基准测试中的写入有 1:1 的对应关系会很有趣。
  • 我只是在你的答案中添加了一点我的答案,因为其余的基本上都是骗人的。
  • 因此,只有当它到达 MC 队列时,才似乎很有可能合并请求(并且 AFAIK MC 肯定会进行这种合并)。
猜你喜欢
  • 1970-01-01
  • 2012-04-07
  • 1970-01-01
  • 2015-08-14
  • 1970-01-01
  • 2019-12-13
  • 1970-01-01
  • 2017-09-26
  • 1970-01-01
相关资源
最近更新 更多