cachegrind 统计 cpu 缓存命中/缺失以评估程序缓存利用效率,而非记录具体内存地址;关键指标为 i1/d1/ll misses,需结合 cg_annotate 定位高缺失函数行,并匹配真实缓存参数分析。

用 cachegrind 看 CPU 缓存行为,不是看“内存访问模式”本身
很多人误以为 cachegrind 能直接显示“哪段代码读了哪些地址”,其实它不记录具体内存地址或访问序列,而是模拟 x86/amd64 的 L1/L2/L3 缓存层级,统计命中、缺失、指令/数据缓存分离等指标。你要观察的其实是「程序对缓存的利用效率」,而非裸地址流。
cachegrind 输出里最关键的三类计数
运行后生成的 cachegrind.out.xxx 文件中,每行以 I(指令)或 D(数据)开头,后面跟三组数字:读/写命中数、读/写缺失数、总访问数。例如:
I1 cache: 32768 B, 64 B, 8-way associative D1 cache: 32768 B, 64 B, 8-way associative LL cache: 8388608 B, 64 B, 16-way associative I1 misses: 12345 D1 misses: 67890 LL misses: 4567
-
I1 misses高 → 指令局部性差,比如函数跳转太散、代码体积大且冷热混杂 -
D1 misses高 → 数据访问跨度大,常见于非连续数组遍历、指针跳跃式访问、结构体字段分散 -
LL misses高 → 整体工作集超出末级缓存,可能触发频繁主存访问,性能瓶颈明显
结合 callgrind_annotate 定位热点函数的缓存表现
光看汇总数字没用,得知道是哪个函数拖垮了缓存。用 callgrind_annotate(注意不是 cachegrind_annotate,Valgrind 没这命令)不行 —— 正确做法是用 cg_annotate 工具解析输出:
cachegrind --cachegrind-out-file=cg.out ./your_program cg_annotate cg.out | head -20
输出中每行会显示函数名、源文件、行号,以及该行贡献的 Ir(指令读)、Dr(数据读)、Dw(数据写)、D1mr(D1 读缺失)、LLmr(LL 读缺失)等列。重点关注 D1mr 和 LLmr 占比高的行,它们往往对应:
- 二维数组按列优先访问(
a[j][i]而非a[i][j]) - 结构体数组中只用某几个字段,但编译器未优化填充(
__attribute__((packed))可能反而更差) - 频繁分配小块堆内存(如循环中
new int[4]),导致地址不连续、缓存行浪费
别忽略 --branch-sim=yes 和缓存配置参数
cachegrind 默认不模拟分支预测器,但如果你怀疑分支误预测影响流水线进而拖累缓存预取,可以加 --branch-sim=yes。更关键的是,它的默认缓存模型(L1 32KB、64B 行、8路)未必匹配你的实际 CPU:
- 用
lscpu查真实缓存参数,再通过--I1=<size>,<line>,<assoc></assoc></line></size>等选项对齐,否则分析结果失真 -
--cache-sim=no会关闭所有缓存模拟,只剩指令计数,失去分析意义 - 不要在调试版(
-O0)上跑cachegrind—— 寄存器溢出、冗余访存会严重污染 D1/LL miss 统计
真正难的是把 D1mr 高和代码结构缺陷联系起来,而不是盯着数字本身;同一段循环,int a[1024] 和 int a[1024][1024] 的 LLmr 可能差两个数量级,但报告不会告诉你“该改用分块”,得你自己对照访问模式反推。











