clflush未必总能引发L1i缓存缺失?还是PMU计数器存在误差?
关于L1指令缓存缺失计数与clflush行为的疑问
问题背景
我编写了一段汇编程序,循环1000万次调用fun函数。fun函数会先执行clflush not_fun刷新not_fun所在的缓存行,再调用not_fun。为确保两个函数不在同一缓存行,我将它们按512字节对齐,理论上每次调用fun都应该触发L1指令缓存(L1i)缺失。
汇编程序
.section .data .section .text .global fun .global not_fun .align 512 not_fun: nop ret .align 512 fun: clflush not_fun mfence call not_fun ret .global _start _start: mov $10000000, %rcx loop: call fun dec %rcx jnz loop xor %rdi,%rdi mov $60, %rax syscall
测试步骤与结果
使用perf stat统计frontend_retired.l1i_miss计数器(Intel手册定义:已退役指令中遭遇L1指令缓存真实缺失的取指操作,同一缓存行的重复请求不计数),执行命令及结果如下:
$ as fun.S -o fun.o $ g++ fun.o -std=c++17 -nostdlib -no-pie -O3 -Wall -Werror -fno-omit-frame-pointer $ sudo perf stat -r 5 -e cycles,instructions,frontend_retired.l1i_miss taskset -c 0 nice -n -20 ./a.out Performance counter stats for 'taskset -c 0 nice -n -20 ./a.out' (5 runs): 7,86,64,83,671 cycles ( +- 0.33% ) 10,39,34,470 instructions # 0.01 insn per cycle ( +- 0.02% ) 67,94,237 frontend_retired.l1i_miss ( +- 0.99% ) 1.81076 +- 0.00394 seconds time elapsed ( +- 0.22% )
实际统计的L1i缺失次数约为679万,远小于1000万次调用次数。同一二进制文件多次运行结果稳定(波动±1万),但重编译后(仅对齐填充不同)结果差异极大(从200万到1000万不等),我想知道:是计数器不准确,还是clflush并未每次都成功刷新not_fun?
问题分析与解答
1. 计数器准确性排除
frontend_retired.l1i_miss计数器本身是可靠的,但要明确它的计数逻辑:仅当取指操作因L1i缺失导致停顿,且对应的指令最终完成退役时,才会被计数。如果缓存行在指令执行前已被预取填充,即使之前clflush失效了缓存行,也不会触发计数。
2. clflush与预取的冲突
clflush的作用是将目标缓存行写回内存并标记为失效,但它无法阻止CPU的指令预取器。mfence仅保证内存读写操作的可见性顺序,对前端的指令预取行为没有约束。在clflush执行后、call not_fun之前,CPU的指令预取器可能已经提前将not_fun的指令加载到L1i中,直接抵消了clflush的效果。- 此外,L2或LLC的硬件预取器也可能在
clflush失效缓存行后,重新预取该缓存行,导致L1i中再次出现有效缓存行。
3. 重编译地址变化的影响
重编译后函数的虚拟地址会发生变化,而CPU的预取器(如基于指令地址的IP预取器)会根据地址模式预测预取目标。如果not_fun的新地址恰好匹配预取器的预测模式,预取命中率会显著提升,L1i缺失次数就会减少;反之则缺失次数接近理论值。这就是重编译后结果波动极大的核心原因。
4. 验证建议
- 在
mfence后添加一段无意义的指令序列(如10个nop),拉长clflush到call not_fun的时间间隔,降低预取提前触发的概率,观察缺失次数是否接近1000万。 - 使用
perf mem record ./a.out+perf mem report分析具体的缺失地址,确认是否全部来自not_fun的入口,排除其他代码的缺失干扰。 - 通过BIOS或
wrmsr指令禁用CPU的预取功能,再测试缺失次数是否符合预期。
内容的提问来源于stack exchange,提问作者Box Box Box Box
相关产品推荐
相关产品推荐

