【问题标题】:Is L2 HW prefetcher really helpful?L2 硬件预取器真的有用吗?
【发布时间】:2020-05-18 11:46:33
【问题描述】:

我在 Whiskey Lake i7-8565U 上分析性能计数器和复制 512 KiB 数据的时间(是 L2 缓存大小的两倍),并且对 L2 HW 预取器的工作存在一些误解。

Intel Manual Vol.4 MSR 中有 MSR 0x1A4 的位 0 用于控制 L2 HW 预取器(1 为禁用)。


考虑以下基准:

memcopy.h:

void *avx_memcpy_forward_lsls(void *restrict, const void *restrict, size_t);

memcopy.S:

avx_memcpy_forward_lsls:
    shr rdx, 0x3
    xor rcx, rcx
avx_memcpy_forward_loop_lsls:
    vmovdqa ymm0, [rsi + 8*rcx]
    vmovdqa [rdi + rcx*8], ymm0
    vmovdqa ymm1, [rsi + 8*rcx + 0x20]
    vmovdqa [rdi + rcx*8 + 0x20], ymm1
    add rcx, 0x08
    cmp rdx, rcx
    ja avx_memcpy_forward_loop_lsls
    ret

main.c:

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

#define ITERATIONS 1000
#define BUF_SIZE 512 * 1024

_Alignas(64) char src[BUF_SIZE];
_Alignas(64) char dest[BUF_SIZE];

static void __run_benchmark(unsigned runs, unsigned run_iterations,
                    void *(*fn)(void *, const void*, size_t), void *dest, const void* src, size_t sz);

#define run_benchmark(runs, run_iterations, fn, dest, src, sz) \
    do{\
        printf("Benchmarking " #fn "\n");\
        __run_benchmark(runs, run_iterations, fn, dest, src, sz);\
    }while(0)

int main(void){
    int fd = open("/dev/urandom", O_RDONLY);
    read(fd, src, sizeof src);
    run_benchmark(20, ITERATIONS, avx_memcpy_forward_lsls, dest, src, BUF_SIZE);
}

static inline void benchmark_copy_function(unsigned iterations, void *(*fn)(void *, const void *, size_t),
                                               void *restrict dest, const void *restrict src, size_t sz){
    while(iterations --> 0){
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
    }
}

static void __run_benchmark(unsigned runs, unsigned run_iterations,
                    void *(*fn)(void *, const void*, size_t), void *dest, const void* src, size_t sz){
    unsigned current_run = 1;
    while(current_run <= runs){
        benchmark_copy_function(run_iterations, fn, dest, src, sz);
        printf("Run %d finished\n", current_run);
        current_run++;
    }
}

考虑运行 2 次已编译的 main.c

MSR:

$ sudo rdmsr -p 0 0x1A4
0

Run:

$ taskset -c 0 sudo ../profile.sh ./bin 

 Performance counter stats for './bin':

    10 486 164 071      L1-dcache-loads                                               (12,13%)
    10 461 354 384      L1-dcache-load-misses     #   99,76% of all L1-dcache hits    (12,05%)
    10 481 930 413      L1-dcache-stores                                              (12,05%)
    10 461 136 686      l1d.replacement                                               (12,12%)
    31 466 394 422      l1d_pend_miss.fb_full                                         (12,11%)
   211 853 643 294      l1d_pend_miss.pending                                         (12,09%)
     1 759 204 317      LLC-loads                                                     (12,16%)
            31 007      LLC-load-misses           #    0,00% of all LL-cache hits     (12,16%)
     3 154 901 630      LLC-stores                                                    (6,19%)
    15 867 315 545      l2_rqsts.all_pf                                               (9,22%)
                 0      sw_prefetch_access.t1_t2                                      (12,22%)
         1 393 306      l2_lines_out.useless_hwpf                                     (12,16%)
     3 549 170 919      l2_rqsts.pf_hit                                               (12,09%)
    12 356 247 643      l2_rqsts.pf_miss                                              (12,06%)
                 0      load_hit_pre.sw_pf                                            (12,09%)
     3 159 712 695      l2_rqsts.rfo_hit                                              (12,06%)
     1 207 642 335      l2_rqsts.rfo_miss                                             (12,02%)
     4 366 526 618      l2_rqsts.all_rfo                                              (12,06%)
     5 240 013 774      offcore_requests.all_data_rd                                     (12,06%)
    19 936 657 118      offcore_requests.all_requests                                     (12,09%)
     1 761 660 763      offcore_response.demand_data_rd.any_response                                     (12,12%)
       287 044 397      bus-cycles                                                    (12,15%)
    36 816 767 779      resource_stalls.any                                           (12,15%)
    36 553 997 653      resource_stalls.sb                                            (12,15%)
    38 035 066 210      uops_retired.stall_cycles                                     (12,12%)
    24 766 225 119      uops_executed.stall_cycles                                     (12,09%)
    40 478 455 041      uops_issued.stall_cycles                                      (12,05%)
    24 497 256 548      cycle_activity.stalls_l1d_miss                                     (12,02%)
    12 611 038 018      cycle_activity.stalls_l2_miss                                     (12,09%)
        10 228 869      cycle_activity.stalls_l3_miss                                     (12,12%)
    24 707 614 483      cycle_activity.stalls_mem_any                                     (12,22%)
    24 776 110 104      cycle_activity.stalls_total                                     (12,22%)
    48 914 478 241      cycles                                                        (12,19%)

      12,155774555 seconds time elapsed

      11,984577000 seconds user
       0,015984000 seconds sys

二。

MSR:

$ sudo rdmsr -p 0 0x1A4
1

Run:

$ taskset -c 0 sudo ../profile.sh ./bin

 Performance counter stats for './bin':

    10 508 027 832      L1-dcache-loads                                               (12,05%)
    10 463 643 206      L1-dcache-load-misses     #   99,58% of all L1-dcache hits    (12,09%)
    10 481 296 605      L1-dcache-stores                                              (12,12%)
    10 444 854 468      l1d.replacement                                               (12,15%)
    29 287 445 744      l1d_pend_miss.fb_full                                         (12,17%)
   205 569 630 707      l1d_pend_miss.pending                                         (12,17%)
     5 103 444 329      LLC-loads                                                     (12,17%)
            33 406      LLC-load-misses           #    0,00% of all LL-cache hits     (12,17%)
     9 567 917 742      LLC-stores                                                    (6,08%)
     1 157 237 980      l2_rqsts.all_pf                                               (9,12%)
                 0      sw_prefetch_access.t1_t2                                      (12,17%)
           301 471      l2_lines_out.useless_hwpf                                     (12,17%)
       218 528 985      l2_rqsts.pf_hit                                               (12,17%)
       938 735 722      l2_rqsts.pf_miss                                              (12,17%)
                 0      load_hit_pre.sw_pf                                            (12,17%)
         4 096 281      l2_rqsts.rfo_hit                                              (12,17%)
     4 972 640 931      l2_rqsts.rfo_miss                                             (12,17%)
     4 976 006 805      l2_rqsts.all_rfo                                              (12,17%)
     5 175 544 191      offcore_requests.all_data_rd                                     (12,17%)
    15 772 124 082      offcore_requests.all_requests                                     (12,17%)
     5 120 635 892      offcore_response.demand_data_rd.any_response                                     (12,17%)
       292 980 395      bus-cycles                                                    (12,17%)
    37 592 020 151      resource_stalls.any                                           (12,14%)
    37 317 091 982      resource_stalls.sb                                            (12,11%)
    38 121 826 730      uops_retired.stall_cycles                                     (12,08%)
    25 430 699 605      uops_executed.stall_cycles                                     (12,04%)
    41 416 190 037      uops_issued.stall_cycles                                      (12,04%)
    25 326 579 070      cycle_activity.stalls_l1d_miss                                     (12,04%)
    25 019 148 253      cycle_activity.stalls_l2_miss                                     (12,03%)
         7 384 770      cycle_activity.stalls_l3_miss                                     (12,03%)
    25 442 709 033      cycle_activity.stalls_mem_any                                     (12,03%)
    25 406 897 956      cycle_activity.stalls_total                                     (12,03%)
    49 877 044 086      cycles                                                        (12,03%)

      12,231406658 seconds time elapsed

      12,226386000 seconds user
       0,004000000 seconds sys

我注意到了柜台:

12 611 038 018 cycle_activity.stalls_l2_missv/s
25 019 148 253 cycle_activity.stalls_l2_miss

建议正在应用 MSR 禁用 L2 硬件预取器。其他 l2/LLC 相关的东西也有很大不同。 在不同的运行中可以重现差异。问题是total time 和周期几乎没有区别:

48 914 478 241 cyclesv/s
49 877 044 086 cycles

12,155774555 seconds time elapsedv/s
12,231406658 seconds time elapsed

问题:
L2 未命中是否被其他性能限制器隐藏?
如果是这样,您能否建议查看哪些计数器来理解它?

【问题讨论】:

  • 根据经验:任何非深度实现的内存副本都是内存绑定的。即使它只命中 L1 缓存。任何内存访问的开销都远远高于 CPU 将两个和两个相加所需的开销。在您的情况下,您甚至使用 AVX 指令来减少每个复制字节的指令量。无论您的数据位于何处(L1、L2、LLC、内存),相关内存组件的吞吐量都会成为您的瓶颈。

标签: c performance assembly x86-64 avx


【解决方案1】:

是的,L2 流媒体真的在很多时候都非常有用。

memcpy 没有任何计算延迟可以隐藏,所以我想它可以让 OoO 执行资源(ROB 大小)处理您从更多 L2 未命中获得的额外负载延迟,至少在这种情况下您可以获得所有L3 命中来自使用适合 L3 的中型工作集 (1MiB),无需预取即可实现 L3 命中。

并且唯一的指令是加载/存储(和循环开销),因此 OoO 窗口包括非常远的需求加载。

如果 L2 空间预取器和 L1d 预取器在这里有帮助,则 IDK。


检验这个假设的预测:让你的数组更大,这样你会得到 L3 未命中,一旦 OoO exec 不足以隐藏加载延迟,你可能会看到总时间的差异一直到DRAM。提前触发硬件预取可能会有所帮助。

硬件预取的另一大好处在于它可以跟上您的计算,因此您可以获得 L2 命中。 (在具有中等长度但没有循环携带的依赖链的计算的循环中。)

在 ROB 容量没有其他压力的情况下,需求负载和 OoO exec 在使用可用(单线程)内存带宽方面可以做很多事情。


另请注意,在 Intel CPU 上,每次缓存未命中都可能导致 依赖 uops 的后端重放(来自 RS/调度程序),L1d 和预计数据到达时,L2 未命中。在那之后,显然核心在等待数据从 L3 到达时乐观地发送垃圾邮件。

(见https://chat.stackoverflow.com/rooms/206639/discussion-on-question-by-beeonrope-are-load-ops-deallocated-from-the-rs-when-thAre load ops deallocated from the RS when they dispatch, complete or some other time?

不是缓存未命中加载本身;在这种情况下,它将是商店说明。更具体地说,端口 4 的存储数据 uop。这在这里无关紧要;使用 32 字节存储和 L3 带宽瓶颈意味着我们不接近每个时钟 1 个端口 4 uop。

【讨论】:

  • @St.Antario:嗯?这是没有意义的;您受内存限制,因此您没有前端瓶颈,因此 LSD 无关紧要。 (它避免了从 uop 缓存中重新获取它们,从而节省了一些电量)。他们仍然在 ROB 中占据空间,直到他们可以退休。它们不是那么重要,但也不容忽视。
  • 让你的数组更大,这样你就会得到 L3 未命中,你可能会看到不同 我用16MiB 缓冲区和10 迭代运行了一些测试,确实得到14,186868883 seconds vs 43,731360909 seconds46,76% of all LL-cache hits vs 99,32% of all LL-cache hits1 028 664 372 LLC-loads1 587 454 298 LLC-loads .
  • @St.Antario:通过注册重命名!这是 OoO exec 中最关键的部分之一,尤其是在像 x86 这样的注册不足的 ISA 上。见Why does mulss take only 3 cycles on Haswell, different from Agner's instruction tables? (Unrolling FP loops with multiple accumulators)。顺便说一句,通常你想要做 2 次加载然后 2 次存储,而不是加载/存储加载/存储。避免或减轻 4k 混叠停顿的机会更大,因为后面的加载(硬件必须检测是否与之前的存储重叠)距离更远。
  • @St.Antario:是的,当然。 Agner Fog 的优化指南还解释了带有寄存器重命名的 OoO exec,维基百科也是如此。顺便说一句,寄存器重命名也避免了 WAW 危害,只留下真正的依赖关系(RAW)。因此加载甚至可以无序完成,而无需等待先前的加载完成写入相同的架构寄存器。是的,唯一的循环承载的 dep 链是通过 RCX,因此该链可以向前运行。这就是为什么地址可以提前准备好,而加载/存储微指令仍然是端口 2/3 吞吐量的瓶颈。
  • 我很惊讶预取对 L3 中的 memcpy 没有帮助。我猜在这种情况下 10/12 LFB 就足够了。不过看起来很奇怪:那里的限制因素是什么?核心 -> L2 时间应该小于 L2 -> L3 时间,所以在我的心智模型中,第二站有更多的缓冲区(更多的总占用率)应该会有所帮助。
【解决方案2】:

是的,L2 硬件预取器非常有用!

例如,在运行 tinymembench 的我的机器 (i7-6700HQ) 上找到以下结果。结果的第一列是所有预取器都打开,第二列结果是 L2 流送器关闭(但所有其他预取器仍然打开)。

此测试使用 32 MiB 源缓冲区和目标缓冲区,这比我机器上的 L3 大得多,因此它将主要测试 DRAM 未命中。

==========================================================================
== Memory bandwidth tests                                               ==
==                                                                      ==
== Note 1: 1MB = 1000000 bytes                                          ==
== Note 2: Results for 'copy' tests show how many bytes can be          ==
==         copied per second (adding together read and writen           ==
==         bytes would have provided twice higher numbers)              ==
== Note 3: 2-pass copy means that we are using a small temporary buffer ==
==         to first fetch data into it, and only then write it to the   ==
==         destination (source -> L1 cache, L1 cache -> destination)    ==
== Note 4: If sample standard deviation exceeds 0.1%, it is shown in    ==
==         brackets                                                     ==
==========================================================================

                                                       L2 streamer ON            OFF
 C copy backwards                                     :   7962.4 MB/s    4430.5 MB/s
 C copy backwards (32 byte blocks)                    :   7993.5 MB/s    4467.0 MB/s
 C copy backwards (64 byte blocks)                    :   7989.9 MB/s    4438.0 MB/s
 C copy                                               :   8503.1 MB/s    4466.6 MB/s
 C copy prefetched (32 bytes step)                    :   8729.2 MB/s    4958.4 MB/s
 C copy prefetched (64 bytes step)                    :   8730.7 MB/s    4958.4 MB/s
 C 2-pass copy                                        :   6171.2 MB/s    3368.7 MB/s
 C 2-pass copy prefetched (32 bytes step)             :   6193.1 MB/s    4104.2 MB/s
 C 2-pass copy prefetched (64 bytes step)             :   6198.8 MB/s    4101.6 MB/s
 C fill                                               :  13372.4 MB/s   10610.5 MB/s
 C fill (shuffle within 16 byte blocks)               :  13379.4 MB/s   10547.5 MB/s
 C fill (shuffle within 32 byte blocks)               :  13365.8 MB/s   10636.9 MB/s
 C fill (shuffle within 64 byte blocks)               :  13588.7 MB/s   10588.3 MB/s
 -
 standard memcpy                                      :  11550.7 MB/s    8216.3 MB/s
 standard memset                                      :  23188.7 MB/s   22686.8 MB/s
 -
 MOVSB copy                                           :   9458.4 MB/s    6523.7 MB/s
 MOVSD copy                                           :   9474.5 MB/s    6510.7 MB/s
 STOSB fill                                           :  23329.0 MB/s   22901.5 MB/s
 SSE2 copy                                            :   9073.1 MB/s    4970.3 MB/s
 SSE2 nontemporal copy                                :  12647.1 MB/s    7492.5 MB/s
 SSE2 copy prefetched (32 bytes step)                 :   9106.0 MB/s    5069.8 MB/s
 SSE2 copy prefetched (64 bytes step)                 :   9113.5 MB/s    5063.1 MB/s
 SSE2 nontemporal copy prefetched (32 bytes step)     :  11770.8 MB/s    7453.4 MB/s
 SSE2 nontemporal copy prefetched (64 bytes step)     :  11937.1 MB/s    7712.1 MB/s
 SSE2 2-pass copy                                     :   7092.8 MB/s    4355.2 MB/s
 SSE2 2-pass copy prefetched (32 bytes step)          :   7001.4 MB/s    4585.1 MB/s
 SSE2 2-pass copy prefetched (64 bytes step)          :   7055.1 MB/s    4557.9 MB/s
 SSE2 2-pass nontemporal copy                         :   5043.2 MB/s    3263.3 MB/s
 SSE2 fill                                            :  14087.3 MB/s   10947.1 MB/s
 SSE2 nontemporal fill                                :  33134.5 MB/s   32774.3 MB/s

在这些测试中,L2 流媒体永远不会变慢,而且通常快两倍。

通常,您可能会在结果中注意到以下模式:

  • 副本通常似乎比填充更受影响。
  • standard memsetSTOSB fill(在这个平台上归结为相同的东西)受影响最小,预取结果仅比没有预取的结果快几个 %。
  • 标准memcpy 可能是这里唯一使用 32 字节 AVX 指令的副本,并且它是受影响最小的副本之一 - 但预取仍然比没有预取快约 40%。

我还尝试打开和关闭其他三个预取器,但它们通常对该基准测试几乎没有可衡量的影响。

【讨论】:

  • (有趣的事实:vmovdqa 是 AVX1,尽管是“整数”。)你认为 OP 的循环提供的带宽比 glibc memcpy 低吗?这就是为什么 12 个 LFB 足以跟上 L3 的需求负载,而无需利用 L2 L3 超级队列中 L2 流媒体可以保持占用的额外 MLP?这大概是你测试的不同之处。 L3 应该以与核心相同的速度运行;你们都有四核 Skylake 客户端等效的微架构,所以可能相似的 L3 延迟?
  • @PeterCordes - 对不起,我可能应该清楚:这个测试是在 32 MiB 缓冲区之间进行的,所以它测试的是 DRAM 命中而不是 L3 命中。我虽然 tmb 输出缓冲区大小,但我发现它没有 - 哎呀!这是故意的:我并没有试图准确解释 OP 的 512 KiB 场景,而只是回答标题问题,即 L2 流媒体在显示它的场景中是否有用。我想我使用了较小的缓冲区大小,我可以或多或少地重现结果(我已经在 cmets 中提到的uarch-bench 中看到了类似的结果)。
  • 我在答案中添加了缓冲区大小。
  • @St.Antario:不,这不是问题。不知道您为什么认为这可能是 一个问题;混合 AVX1 和 AVX2 指令并没有任何惩罚。我评论的重点是这个循环只需要 AVX1,但这个答案提到使用 AVX2 指令。英特尔碰巧在引入 AVX2 的同时将 L1d 加载/存储数据路径扩大到 32 字节,因此如果您正在执行运行时调度,您可以使用 AVX2 的可用性作为选择 memcpy 实现的一部分......跨度>
  • 你是如何关闭预取器的?是哪一个?是software.intel.com/en-us/articles/…吗?论坛software.intel.com/en-us/forums/intel-isa-extensions/topic/…说有些位有不同的含义。
猜你喜欢
  • 2012-04-26
  • 2012-09-14
  • 2013-11-30
  • 1970-01-01
  • 2023-03-13
  • 2019-09-21
  • 2015-11-13
  • 2011-12-15
相关资源
最近更新 更多