为何高L1缓存缺失率程序实测缺失率接近0%?
我正尝试编写一款L1缓存缺失率尽可能高的程序,使用Intel Core i7处理器的MEM_LOAD_RETIRED.L1_MISS和MEM_LOAD_RETIRED.L1_HIT性能计数器统计缺失率,已修改Linux内核以在上下文切换时精确测量,且已禁用硬件预取器。
现有代码如下:
#define LINE_SIZE 64 #define CACHE_SIZE 4096 * 8 #define MEM_SIZE CACHE_SIZE * 64 void main(int argc, char* argv[]) { volatile register char* addr asm ("r12") = mmap(0, MEM_SIZE, PROT_READ|PROT_WRITE, MAP_ANONYMOUS|MAP_PRIVATE, -1, 0); volatile register unsigned long idx asm ("r13") = 0; volatile register unsigned long store_val asm ("r14") = 0; volatile register unsigned long x64 asm ("r15") = 88172645463325252ull; while(1) { x64 ^= x64 << 13; x64 ^= x64 >> 7; x64 ^= x64 << 17; store_val = addr[x64 % MEM_SIZE]; } }
该代码每次循环仅一次内存访问,但实测缺失率接近0%;即使移除xorshift逻辑,改为每次步长64的线性访问,缺失率仍未达预期的接近100%。但使用callgrind测试线性访问时,能得到预期的99.9%缺失率,而perf工具(命令:perf stat -r 10 -B -e mem_load_retired.l1_miss,mem_load_retired.l1_hit ./thrasher)的结果与内核统计一致。
原因分析
L1缓存的包容性架构:Intel Core i7的L1数据缓存采用包容性设计,即末级缓存(LLC)中存在的缓存行,L1会保留其副本(或当L1缺失但LLC命中时,缓存行会被回填至L1)。代码中
MEM_SIZE为32KB*64=2048KB,而Core i7的LLC容量通常远大于此(如早期型号为8MB),所有内存数据可完全容纳于LLC中。每次L1缺失后,数据会从LLC快速回填至L1,后续重复访问同一地址时L1直接命中,导致缺失率极低。而callgrind默认仅模拟L1缓存,未考虑LLC的存在,因此会输出接近100%的缺失率。线性访问的LLC缓存复用:即使改为步长64的线性访问,由于LLC已缓存全部数据,L1每次缺失后都会从LLC加载数据并缓存。虽然L1容量仅32KB,但LLC的高命中率使得被替换出L1的缓存行可随时回填,重复访问时L1能快速命中,无法达到预期的100%缺失率。
性能计数器的统计逻辑:
MEM_LOAD_RETIRED.L1_MISS统计的是已退休加载指令中,在L1缓存未命中的次数。当数据存在于LLC时,L1缺失后会触发缓存行回填,后续对同一缓存行的访问会直接命中L1,这类访问不会被计数为L1缺失。无限循环会重复访问相同缓存行,进一步拉高L1命中率。
内容的提问来源于stack exchange,提问作者arn

