callgrind 默认统计指令执行次数(ir),即cpu读取并执行的每条汇编指令次数,是定位热点函数/循环的核心指标;ir非源码行数,需-g编译+callgrind_annotate或kcachegrind分析。

Callgrind 默认统计的就是指令执行次数(Ir)
Valgrind 本身不直接输出“指令数”,但 callgrind 工具的核心指标就是 Ir(Instruction reads),即 CPU 执行的每条指令被读取的次数 —— 这等价于“该指令被执行了多少次”。它不是时钟周期或 wall-time,而是最底层的执行频度统计,对定位热点函数/循环非常有效。
关键点:Ir 不是“代码行数”或“源码语句数”,而是汇编指令粒度。一个 for 循环体可能展开成多条指令,每条都单独计数。
-
valgrind --tool=callgrind ./a.out运行后,默认生成callgrind.out.<pid></pid>,里面就含 Ir 数据 - 不用额外开关,Ir 就是 callgrind 的默认主指标;其他如缓存未命中(D1mr、LLmr)需显式启用模拟(如
--cache-sim=yes) - 如果程序很快退出,Ir 总数可能极小(比如只有几万),说明没跑进真正逻辑,要检查参数或输入是否触发了目标路径
用 callgrind_annotate 看 Ir 分布
callgrind_annotate 是解析 callgrind.out.* 文件并按 Ir 排序输出的命令行工具。它不画图,但文本清晰、可管道处理,适合快速定位。
- 看全局函数级 Ir:
callgrind_annotate callgrind.out.12345→ 输出按 Ir 降序排列的函数列表,首列数字就是该函数贡献的总 Ir - 绑定源码行(需带
-g编译):callgrind_annotate --auto=yes callgrind.out.12345→ 自动匹配源文件,把 Ir 拆到每一行 C/C++ 代码上 - 包含调用链代价:
callgrind_annotate --inclusive=yes callgrind.out.12345→ 函数 A 的 Ir 包含它调用的所有子函数的 Ir,适合看“模块总开销” - 避免误读:Ir 高 ≠ 耗时长(尤其在分支预测好、流水线深的 CPU 上),但 Ir 高 + 缓存未命中高(
D1mr)往往真慢
多线程下 Ir 统计容易漏掉主线程或线程间干扰
默认情况下,callgrind 把所有线程的 Ir 合并在一个文件里,但线程切换、锁竞争、TLS 访问会导致 Ir 分布失真 —— 比如 mutex 等待期间的空转循环也被计入 Ir,但它实际没做有用工作。
- 开启线程分离:
valgrind --tool=callgrind --separate-threads=yes ./a.out→ 每个线程写独立的 Ir 计数,便于比对各线程负载是否均衡 - 注意 PID 变化:多线程程序 fork 或 pthread_create 后,新线程的 Ir 会记在新 PID 名下的
callgrind.out.*中,别只盯最初那个文件 - dump 中途快照:
callgrind_control -d可在运行中强制刷出当前累计 Ir,适合分析长周期服务中某阶段的指令热点 - Ir 在线程切换点附近可能跳变(因上下文保存/恢复指令被重复计),这不是 bug,是模拟器必须付出的代价
KCachegrind 更直观但依赖 Qt 和符号表
kcachegrind 是图形化前端,能把 callgrind.out.* 渲染成调用图、火焰图、源码着色视图,对 Ir 的理解帮助极大,但有几个硬性前提:
- 必须安装 Qt5+(Ubuntu/Debian 下
sudo apt install kcachegrind) - 二进制必须含调试符号(
g++ -g编译),否则函数名显示为???,源码行无法关联 - 打开后默认按 Ir 排序,点击 “Self” 列可看函数自身 Ir(不含子调用),点 “Inclusive” 看总 Ir
- 右键函数 → “Jump to source” 可跳转到对应行,旁边数字就是该行贡献的 Ir —— 这是最接近“哪一行最忙”的答案
Ir 统计本身不区分用户态/内核态指令,系统调用(如 read、write)内部的 Ir 也会被计入,但你通常看不到它们的符号 —— 这部分 Ir 属于黑盒,只能从调用方推断开销。











