编译需加-g -O0 -fno-omit-frame-pointer,运行加--dump-instr=yes --compress-strings=no,解析用callgrind_annotate --auto=yes --inclusive=yes --threshold=1;异常性能代价体现为栈展开相关函数高指令数。

直接跑 valgrind --tool=callgrind 就能开始,但没加关键参数的话,生成的报告基本没法看——函数名缺失、行号错乱、调用关系扁平化,等于白跑。
编译时必须带 -g -O0 且禁用帧指针优化
Callgrind 依赖调试符号定位函数和行号。如果用 -O2 或更高优化级别,inline 和函数折叠会让调用栈失真;不加 -g 则报告里全是 ???。
-
-g:生成 DWARF 调试信息,否则callgrind_annotate无法映射到源码 -
-O0:关闭所有优化,避免编译器把小函数内联掉,导致调用计数归零或合并 -
-fno-omit-frame-pointer:保留帧指针,让 Callgrind 能正确回溯调用链(尤其对递归/异常路径很重要)
正确编译命令示例:g++ -g -O0 -fno-omit-frame-pointer -o myapp main.cpp
valgrind --tool=callgrind 的关键运行参数
只输 valgrind --tool=callgrind ./myapp 会生成默认文件 callgrind.out.<pid></pid>,但默认配置下它不会记录调用关系细节,也不区分库函数和业务代码。
-
--dump-instr=yes:启用指令级统计(必要,否则只统计函数调用次数,不统计耗时) -
--collect-jumps=yes:收集跳转信息,辅助分析分支预测开销(可选,但对 if/loop 热点有用) -
--compress-strings=no:防止函数名被压缩成哈希,方便后续callgrind_annotate识别 -
--log-file=callgrind.log:把 stderr 输出重定向,避免干扰程序自身日志
完整运行命令:valgrind --tool=callgrind --dump-instr=yes --compress-strings=no --log-file=callgrind.log ./myapp
用 callgrind_annotate 解析报告时别漏掉 --auto=yes
生成的 callgrind.out.* 是二进制格式,必须用配套工具解析。直接 callgrind_annotate callgrind.out.1234 会报错或输出空内容——因为默认不自动关联源码路径。
-
--auto=yes:自动查找当前目录下的源文件(要求编译时用了相对路径,且源码未移动) -
--inclusive=yes:显示包含子函数耗时的“总耗时”,否则只显示函数自身指令数(容易误判热点) -
--threshold=1:过滤掉占比低于 1% 的调用,避免被std::vector::push_back这类模板展开刷屏
推荐解析命令:callgrind_annotate --auto=yes --inclusive=yes --threshold=1 callgrind.out.1234
throw 异常的性能代价在 Callgrind 报告里怎么识别
异常处理不是“慢在 throw 那一行”,而是慢在栈展开(stack unwinding)过程。Callgrind 不会标出“这里抛了异常”,但它会暴露出异常路径上大量未被优化的函数调用和指令执行。
- 对比正常返回路径和异常路径的
Ir(指令数):异常路径通常高 5–10 倍,尤其集中在__cxa_throw、__gxx_personality_v0、std::terminate等运行时函数 - 注意
fun2_throwError在报告中可能显示为 0 指令数——因为编译器把 throw 编译成跳转到运行时函数,真正耗时在后者 - 如果看到大量
???函数名 + 高Ir,大概率是异常展开过程中调用的 libstdc++ 内部函数,此时需用-g重新编译标准库(不现实),或改用perf record -e instructions:u辅助交叉验证
真正要优化的不是 throw 本身,而是减少异常触发频率,或用状态码替代可预期错误——Callgrind 报告里的高 Ir 区域就是你该砍掉的逻辑分支。



















