You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何mem_load_retired.l1_hit与l1_miss之和不等于总加载数?

缓存性能计数器差异疑问:mem_inst_retired.all_loads ≠ l1_hit + l1_miss

问题背景

我正在研究缓存对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缓存,所有常规加载都会经过缓存。查阅Intel相关文档后,未发现能推翻该想法的内容。

然而运行特定代码时,发现两者之和远小于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统计结果如下:

~$ 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_hit和l1_miss之和不等于all_loads?

有趣的是,当调整内存加载步长使几乎所有加载都是命中或缺失时,结果趋近于all_loads ≈ l1_hit + l1_miss,仅在中间情况时等式不成立。

编辑:我在Kaby Lake和Ice Lake两款CPU上测试,结果一致。


解答

核心原因:计数器统计范围的差异

三个性能计数器的统计逻辑并不完全覆盖同一组加载指令:

  • mem_inst_retired.all_loads:统计所有成功退休的加载指令,无论数据来源是寄存器、各级缓存、预取缓冲(Fill Buffer/LFB)还是内存。
  • mem_load_retired.l1_hit:仅统计从L1数据缓存中直接获取数据并退休的加载指令,不包含从预取缓冲、临时存储结构获取数据的情况。
  • mem_load_retired.l1_miss:统计需要从L1缓存之外(L2/L3/内存)获取数据并退休的加载指令,同样排除了预取缓冲命中的场景。

测试场景具体分析

你使用32B步长访问内存,刚好是Intel L1缓存行(64B)的一半,这种高度可预测的访问模式会触发CPU的L1数据预取器提前将后续缓存行预取到Fill Buffer(行填充缓冲)中。当加载指令访问这些预取完成的数据时,会直接从Fill Buffer读取,不需要经过L1数据缓存的常规命中流程——因此这部分加载不会被l1_hit或l1_miss统计,但加载指令本身正常退休,会被all_loads完整统计。

你的测试数据中,约50%的加载属于这种情况,导致l1_hit + l1_miss远小于all_loads。

步长影响的逻辑

  • 极小步长(如1B):所有访问都落在同一个缓存行内,加载直接命中L1缓存,l1_hit会趋近于all_loads。
  • 极大步长(如64B及以上):每个访问都对应全新的缓存行,预取器无法及时预取,加载会触发L1缺失,l1_miss会趋近于all_loads。
  • 中间步长(如32B):预取器刚好能提前将数据预取到Fill Buffer,大量加载跳过L1缓存的常规命中统计,才会出现两者之和远小于all_loads的情况。

内容的提问来源于stack exchange,提问作者MC ΔT

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 13:05:14