Callgrind不提供内置diff功能,需手动比对两次一致参数生成的callgrind.out.*文件;用callgrind_annotate转文本后diff,或gprof2dot导出dot文件比对调用结构变化。

Callgrind 本身不提供直接的 diff 功能,必须靠外部工具或手动比对生成的 callgrind.out.* 文件。 它只负责采集单次运行的调用统计(指令数、调用次数、被调用者信息等),不内置版本对比逻辑。想看出“加了异常处理后慢了多少”,得自己搭比对链路。
怎么生成可比对的 callgrind 输出文件
两次运行必须用完全一致的参数,否则数据不可比。关键点:
- 统一使用
--dump-instr=yes和--collect-jumps=yes(如需跳转分析); - 固定输出名,避免进程号干扰:用
--callgrind-out-file=callgrind.base和--callgrind-out-file=callgrind.exp; - 确保程序输入、环境变量、线程调度尽量一致(尤其多线程下建议加
--separate-threads=no关闭线程分离); - 编译时带
-g,否则callgrind_annotate无法映射到源码行。
用 callgrind_annotate 分别生成人可读报告再比对
callgrind_annotate 是唯一官方支持的文本化工具,它把二进制 profile 数据转成按函数/文件排序的耗时占比列表。两次运行分别执行:
callgrind_annotate --auto=yes --threshold=0.1 callgrind.base > base.txt callgrind_annotate --auto=yes --threshold=0.1 callgrind.exp > exp.txt
然后用 diff base.txt exp.txt 或 VS Code 的 Compare Files 功能看差异。注意:
-
--threshold=0.1过滤掉占比太小的噪音项,聚焦主要变化; -
--auto=yes自动识别源码路径,避免手动指定--include; - 如果函数内联较多,
callgrind_annotate默认不展开内联,加--inclusive=yes才能看清总开销归属。
用 gprof2dot + diff 图形节点差异(适合函数调用结构变化)
当你要看“是否新增了某条调用路径”(比如 throw 触发了额外的 __cxa_throw 调用链),纯文本不够直观。流程是:
- 用
gprof2dot.py -f callgrind callgrind.base | dot -Tpng -o base.png生成调用图; - 同理生成
exp.png; - 用图像 diff 工具(如
imgdiff)或肉眼比对节点增减、边粗细变化; - 更准的做法是先导出 dot 文本:
gprof2dot.py -f callgrind callgrind.base > base.dot,再用diff base.dot exp.dot—— dot 文件里每行是一个调用关系,增删改一目了然。
这种比对方式能暴露隐藏在底层的间接开销,比如异常抛出导致的栈展开函数批量激活,而文本报告可能只显示顶层函数变慢。
容易忽略的陷阱:指令计数 ≠ 实际耗时,且两次运行不能简单相减
Callgrind 统计的是模拟 CPU 执行的指令数(Ir),不是真实时间。这意味着:
- 即使两次运行
Ir差 20%,也不代表性能差 20% —— 可能一次命中 L1 cache 多,一次频繁访存; -
callgrind.out.*文件里的时间戳、进程 ID、内存地址全是动态的,直接diff二进制文件毫无意义; - 若程序有随机性(如 hash 表 rehash、std::random_device),两次
Ir值天然浮动,应跑 3–5 次取中位数再比对; - 异常路径的开销常藏在
__cxa_allocate_exception、__cxa_throw、__cxa_begin_catch等符号里,这些默认不显示在源码行,需加--show=any参数让callgrind_annotate强制列出。


















