Cachegrind不直接提供缓存命中率,需手动计算:D1命中率=1−D1mr/Drefs,LL命中率=1−LLmr/Drefs;关键指标是Ir(指令数)、D1mr(一级数据缓存缺失数)和LLmr(末级缓存缺失数),应结合源码行级标注分析局部热点而非依赖平均值。

看懂 Cachegrind 输出里的三个核心计数器
Cachegrind 不是告诉你“程序慢”,而是告诉你“为什么慢”——关键在 CPU 缓存行为。它模拟 I1(指令缓存)、D1(数据一级缓存)和 LL(二级缓存),但日常调优真正要盯死的只有三个数字:Ir、D1mr、LLmr。
Ir 是指令数(Instruction references),反映代码执行密度;D1mr 是 D1 缓存缺失次数(Data L1 miss rate),直接关联访存延迟;LLmr 是二级缓存缺失次数(Last Level miss rate),一次 LL miss 往往意味着上百周期停顿。这三个值在 cachegrind.out.xxx 文件顶部汇总行里就能看到,比如:
I refs: 12,345,678 D refs: 3,456,789 D1 misses: 234,567 LL misses: 12,345
重点关注的是后两者的**比例**,而不是绝对值:用 D1mr = D1 misses / D refs 算出 D1 缺失率,超过 5% 就值得查;LLmr = LL misses / D refs 超过 0.5% 通常说明数据局部性严重不足。
函数级分析时优先过滤 high Ir + high D1mr 组合
用 cachegrind_annotate 解析输出后,别从头扫到尾。直接按 Ir 排序,挑出前 10 名函数,再逐个检查它们的 D1mr 和 LLmr 值。
- 如果某个函数
Ir高但D1mr - 如果
Ir中等但D1mr> 8%,重点看它是否在遍历大数组、结构体数组未对齐、或频繁跳转访问非连续内存 - 如果
LLmr显著高于D1mr(比如差一个数量级),说明数据根本没留在 L1/L2,可能正在反复刷 cache line,或是工作集远超 LLC 容量
示例命令:cachegrind_annotate --auto=yes --show=Ir,D1mr,LLmr cachegrind.out.12345 | head -n 20
别被 “Cache hit rate” 这种平均值骗了
Cachegrind 默认不显示 hit rate,但有人会自己算 (1 − D1 misses / D refs) 当作“命中率”。这很危险——平均值掩盖了热点偏差。
真实问题往往藏在局部:一段 20 行的循环体可能贡献了 70% 的 D1 miss,而其余 99% 的代码 miss 几乎为 0。这时整体 D1mr 看起来才 2%,但那 20 行实际 miss 率是 40%。
所以必须结合源码行级标注看,关注 cachegrind_annotate 输出中带具体文件名和行号的条目,尤其注意:
- 循环体内重复出现的
mov、lea或cmp指令附近的数据访存 - 结构体成员访问(如
arr[i].field)若field偏移较大,容易跨 cache line - 指针数组(
ptrs[i])若地址随机分布,LLmr会飙升
LL miss 高但 D1 miss 低?大概率是写分配策略惹的祸
一种容易被忽略的情况:D1mr 很低(比如 0.3%),但 LLmr 却高(比如 3.1%)。这不是数据局部性差,而是写操作触发了 write-allocate —— CPU 在写未命中时先读整个 cache line 到 L1,再改,最后写回 LLC。
此时真正瓶颈不在读,而在写回带宽和 LLC 占用。验证方式:
- 用
--cachegrind-out-file=xxx输出后,加参数--branch-sim=yes再跑一次,看Dr(data reads)和Dw(data writes)比例 - 如果
Dw / (Dr + Dw)> 40%,且写操作集中在少数几行(比如循环内a[i] = x),就该考虑用 non-temporal store(如_mm_stream_si32)绕过 cache - 或者把写操作 batch 化,减少写频次
这种模式在图像处理、矩阵初始化等场景特别常见,但 cachegrind 默认不区分读写 miss,必须靠人工交叉比对。
真正难的不是看懂数字,而是判断哪一行代码的访存模式让 cache line 反复进出 L1 和 LLC。工具只报现象,归因还得靠你对数据布局和访问顺序的理解。


















