callgrind_annotate 默认按总执行指令数(ir)降序排序,而非调用频次;要按调用次数排序需显式指定 --sort=calls 且必须加 --auto=yes。

callgrind_annotate 默认排序只看指令数,不是调用频次
默认用 callgrind_annotate callgrind.out.12345 输出的函数列表,是按「总执行指令数」降序排的,不是按「被调用次数」。哪怕一个函数只有几行、每行 1 条指令,只要它被调了 10 万次,实际开销可能远超一个只调 1 次但含 10 万条指令的函数——但默认报告里它会排得很靠后。
真正想定位“调用特别频繁”的函数,得换排序维度。Callgrind 原生支持按调用次数(calls)或每调用平均指令数(Ir per call)排序,但需要显式指定。
-
callgrind_annotate --auto=yes --sort=calls callgrind.out.12345:按总调用次数从高到低排 -
callgrind_annotate --auto=yes --sort='Ir per call' callgrind.out.12345:按每次调用平均指令数排,适合找“小函数但高频”或“内联展开多”的热点 -
--auto=yes必须加,否则--sort参数会被忽略 - 如果输出里某函数的
calls列全是0或空,说明 Callgrind 没捕获到调用事件——大概率是编译时没加-g,或者该函数被内联(-O2以上常见),此时需加--collect-jumps=yes或改用kcachegrind查看调用图
kcachegrind 图形界面里怎么快速筛高频调用
kcachegrind 比命令行更直观,但默认视图也不直接暴露“调用次数”。关键操作藏在列设置和视图切换里:
- 启动后右键表头 → “Configure Columns…” → 勾选
Calls和Calls per second,这两列才是调用频次的核心指标 - 点击
Calls列标题可升/降序排列,直接看到谁被调最多 - 切到 “Callee Map” 视图(顶部菜单 View → Callee Map),它把每个函数画成矩形块,面积正比于被调用次数——一眼识别出“调用黑洞”
- 注意:如果某个函数在
Calls列显示为1但实际被循环调用多次,说明 Callgrind 把它当作了“入口函数”而非“被调函数”,这时要检查是否用了-fno-omit-frame-pointer编译(尤其-O2下帧指针可能被优化掉)
为什么有些函数明明高频调用却不出现在 calls 统计里
这是最常踩的坑:你确认代码里循环调了 10 万次 std::vector::push_back,但 callgrind_annotate 的 Calls 列却是 0 或极小值。原因通常有三个:
-
std::vector::push_back是模板函数,编译后符号名带大量模板参数(如_ZNSt6vectorIiSaIiEE9push_backERKi),而 Callgrind 默认不解析 C++ 符号修饰(mangled name)。解决方法:加--demangle=yes(kcachegrind默认开启,命令行需手动加) - 函数被编译器内联(尤其是
-O2或-O3下的 trivial 函数),Callgrind 就看不到“调用动作”,只看到内联后的指令流。此时Calls为 0 是正常现象,应转看Ir per call或反汇编(--dump-instr=yes)确认是否真被展开 - 跨动态库调用(如 dlopen 加载的 .so)未启用符号解析:Callgrind 默认不追踪 PLT/GOT 调用,需配合
--trace-children=yes和确保目标 so 编译带-g
多线程程序中统计每个线程的调用频次
默认情况下,Callgrind 把所有线程的调用数据混在一起写进一个 callgrind.out.PID 文件,无法区分线程粒度。要分开统计:
- 运行时加
--separate-threads=yes:valgrind 会为每个线程生成独立文件,如callgrind.out.12345-01(主线程)、callgrind.out.12345-02(子线程1)等 - 对每个文件单独跑
callgrind_annotate --sort=calls,就能看到各线程自己的高频函数 - 注意:
--separate-threads=yes会让输出体积翻倍(甚至更多),且kcachegrind默认只打开一个文件,需手动依次加载多个callgrind.out.*文件对比 - 如果线程生命周期短(比如只存在几毫秒),可能来不及生成完整文件——这时建议用
perf record -g -e cycles,instructions,branches替代,它对短生命周期线程更友好
真正难的是区分“高频调用”和“高频开销”:一个函数被调 100 万次,每次只干 1 条指令,它不该是优化重点;而一个被调 100 次、每次耗 10 万条指令的函数,才值得深挖。Callgrind 提供的维度很多,但默认不帮你做这层判断——得自己对照 Calls、Ir、Ir per call 三列交叉看。











