caching - 为什么 mem_load_retired.l1_hit 和 mem_load_retired.l1_miss 没有添加到加载总数中?

标签 caching x86 x86-64 cpu-architecture perf

我正在研究缓存对 x86-64 CPU 性能的影响。我一直在使用 Linux 的 perf 来监控缓存命中/未命中率,特别是这些计数器:

  • mem_inst_retired.all_loads
  • mem_load_retired.l1_hit
  • mem_load_retired.l1_miss

我期望 all_loads ~= l1_hit + l1_miss,因为加载指令可能命中或错过 L1 缓存 - 没有其他选择,因为所有常规加载都会经过缓存。我已阅读英特尔文档here据我所知,这并没有说太多,也没有什么可以反驳我的想法。

但是,在运行某些代码时,我注意到总和远低于 all_loads
例如,以下程序集将 10 亿个内存位置相加,步长为 32B:

.intel_syntax noprefix
.globl main
main:
    sub     rsp, 8     # Stack alignment for call
    push    r12
    push    r13
    mov     r12, 0xfffffff     # Array size

    mov     rdi, r12
    call    malloc              # Allocate array
    mov     r13, rax          # Save array pointer

    mov     rdi, r13          # Write to array to force page-in
    mov     rsi, 42
    mov     rdx, r12
    call    memset

    mov     rcx, 1000000000    # Loop counter
    mov     rdx, 0             # Index sequence start
    mov     rax, 0             # Result accumulator

.p2align 4      # Skylake JCC alignment issue
loop:
    mov     rdi, rdx
    and     rdi, r12      # Mask index to array size
    movzx   rsi, BYTE PTR [r13+rdi]   # Read from array index
    add     rax, rsi

    lea     rdx, [rdx+32] # Generate next array index
    dec     rcx        # Loop counter & condiiton
    jnz     loop

    pop     r13
    pop     r12
    add     rsp, 8
    ret

它产生以下性能结果:

~$ perf stat -e instructions,cycles,mem_inst_retired.all_loads,mem_load_retired.l1_hit,mem_load_retired.l1_miss ./test

 Performance counter stats for './test':

     7,000,215,742      instructions:u                   #    1.25  insn per cycle            
     5,589,048,737      cycles:u                                                              
       998,622,922      mem_inst_retired.all_loads:u                                          
        17,215,080      mem_load_retired.l1_hit:u                                             
       424,118,595      mem_load_retired.l1_miss:u                                            

       1.939187022 seconds time elapsed

       1.826889000 seconds user
       0.112177000 seconds sys

all_loads 正如预期的那样大约为 10 亿(尽管有点低,但可能是一些采样人工制品)。然而,l1_hit + l1_miss 约为 4.5 亿 - 似乎约有 50% 的负载下落不明。

是什么导致 l1_hitl1_miss 的总和不等于 all_loads

有趣的是,如果内存加载步幅发生变化,导致几乎所有加载都命中或未命中,则结果往往会趋于 all_loads ~= l1_hit + l1_miss。只有在中间地带,平等才会被打破。

编辑:我在两个 CPU 上进行了测试:Kaby Lake 和 Ice Lake。两者显示相同的结果。

最佳答案

正如 Margaret Bloom 在评论中指出的那样,加载到相同的缓存行,因为已经未完成的缓存行可以“命中”该 LFB,而不是分配新的缓存行。事实证明,既不计算 l1_hit 也不计算 l1_miss 。它有一个单独的事件 mem_load_retired.fb_hit 。 (l1_miss 只计算导致对 L2 的新请求的指令,而不是同时计算 LFB 命中,这可能是件好事。另请注意,LFB 可能被传出存储占用,包括 NT 存储,因此因此导致的 LFB 命中也被计算在内。可能;这并不总是只是由于多次加载所致。)

您的代码跨度为 32 字节,因此每 64 字节行加载 2 次;第二个通常是 LFB 热门。 (如果硬件预取已经请求,第一个也可能是 LFB 命中,这可能解释了 LFB 命中多于未命中的原因。)

在我的 Skylake i7-6700k 上,使用此测试程序,mem_inst_retired.all_loads 仅比 mem_load_retired.fb_hit + mem_load_retired.l1_hit + mem_load_retired.l1_miss 大 0.6% 左右。

因此,其中的区别仍然有点神秘,即 mem_inst_retired.all_loads 计算的内容,而不是三个更具体的计数器中的任何一个。我预计它们会更接近完全相等,特别是对于 --all-user:u 事件,这样在编程或收集计数器时就不会出现噪音1

使用 perf stat --no-big-num --all-user -e ... 可以轻松地将数字复制/粘贴到 calc 中,我在一次运行中得到了 hit+miss+LFB = 994.188M 与 all-loads = 999.958M 计数。所以总和低了 0.58%。在重复运行时,这是非常典型的,未命中/命中/LFB 计数器的总和比 mem_inst_retired.all_loads 低一小部分。

再运行几次:

$ perf stat --all-user --no-big-num -e task-clock,page-faults,instructions,cycles,mem_inst_retired.all_loads,mem_load_retired.l1_hit,mem_load_retired.l1_miss,mem_load_retired.fb_hit ./a.out

 Performance counter stats for './a.out':

           1673.21 msec task-clock                       #    0.997 CPUs utilized             
               183      page-faults                      #  109.371 /sec                      
        7000141672      instructions                     #    1.56  insn per cycle            
        4475942186      cycles                           #    2.675 GHz                       
         999892622      mem_inst_retired.all_loads       #  597.590 M/sec                     
          10563966      mem_load_retired.l1_hit          #    6.314 M/sec                     
         449822478      mem_load_retired.l1_miss         #  268.838 M/sec                     
         533816318      mem_load_retired.fb_hit          #  319.038 M/sec                     

       1.677680356 seconds time elapsed

       1.647640000 seconds user
       0.023197000 seconds sys


$ perf stat --all-user --no-big-num -e task-clock,page-faults,instructions,cycles,mem_inst_retired.all_loads,mem_load_retired.l1_hit,mem_load_retired.l1_miss,mem_load_retired.fb_hit ./a.out

 Performance counter stats for './a.out':

           1649.17 msec task-clock                       #    1.000 CPUs utilized             
               182      page-faults                      #  110.359 /sec                      
        7000141486      instructions                     #    1.58  insn per cycle            
        4419739785      cycles                           #    2.680 GHz                       
         999850616      mem_inst_retired.all_loads       #  606.275 M/sec                     
           9372903      mem_load_retired.l1_hit          #    5.683 M/sec                     
         450146459      mem_load_retired.l1_miss         #  272.953 M/sec                     
         534244404      mem_load_retired.fb_hit          #  323.947 M/sec                     

       1.649504255 seconds time elapsed

       1.634275000 seconds user
       0.013270000 seconds sys

(我通常在 taskset -c 1 下运行单线程测试,以确保没有 cpu-migration 事件,但在空闲系统上短期运行时通常不会发生这种情况。)

我的 EPP /sys/devices/system/cpu/cpufreq/policy*/energy_performance_preference ) 设置为 balance-performance (不完整 performance ),因此硬件 P 状态管理在此内存密集型工作负载上的时钟频率降至 2.7GHz。由于 --all-user ,计算出的 2.68GHz 仅计算用户空间周期,但 task-clock 是挂钟时间。 (这在一定程度上减少了每核内存带宽,因为非核心速度减慢,使得延迟 x max_in-flight_lines 成为单核内存带宽的限制因素。这对于本实验来说不是问题,但它是其他不明显但可见的问题在此性能数据中。我的 i7-6700k 有双 channel DDR4-2666,运行 Arch Linux,内核 6.4.9)

脚注 1:即使 --all-user 也并不完美。 @John McCalpin 评论道:

The details vary by implementation, but there are lots and lots of little gotchas when trying to make performance counts accurate in the presence of unnecessary user/kernel crossings. Ice Lake Xeon will undercount if the counters are configured for user-only or kernel-only (errata ICX14). Going through the kernel is OK for coarse measurements (~10%), but for detailed studies of the consistency of different events it is best to avoid leaving user-space.

在让内核对计数器进行编程之后,您可以通过 rdpmc 收集用户空间中的计数来完成此操作。 (也许通过 perf_event_open 。)

可以通过较短的测试间隔来避免该核心上的中断,否则您需要研究 isolcpus= Linux 内核启动选项,以便您可以测试超过一个时间片的时间,同时仍然避免在定时/期间进行任何用户/内核转换分析区域。

关于caching - 为什么 mem_load_retired.l1_hit 和 mem_load_retired.l1_miss 没有添加到加载总数中?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/77052435/

相关文章:

linux - 删除 USB 大容量存储时保留缓存

linux - Linux AMD64 中的 fs/gs 寄存器如何使用?

c++ - 返回元组时 GCC/Clang x86_64 C++ ABI 不匹配?

memory - 使用分段 : how? 的 64 TB 虚拟内存,用于 32 位 x86

c++ - SSE 和 iostream : wrong output for floating point types

c - 如何引用在静态库中声明的变量?

caching - 当键具有不同的时间范围并且缓存已满时,memcached 过期行为是什么?

algorithm - 二叉树的最佳填充顺序

javascript - 无法向服务 worker 发布消息,因为 Controller 值为空

java - 任何面向对象的灵活 Java x86 反汇编程序库?