为何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_loadsmem_load_retired.l1_hitmem_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

