必须用 gcc -g -o0 编译单元测试二进制以确保 cachegrind 分析准确,避免编译器优化导致函数调用关系丢失、源码行无法映射及缓存指标失真。

用 cachegrind 跑单元测试前必须关掉编译器优化
直接拿 gcc -O2 编译的测试二进制跑 cachegrind,看到的指令数(Ir)和缓存行为会严重失真——编译器内联、循环展开、死代码消除都会让原始函数调用关系消失,cachegrind.out.pid 里根本找不到你写的测试用例函数。必须用 -g -O0 重新编译,确保符号完整、控制流可追溯。
gcc -g -O0 -o test_unit test_unit.c- 如果用了 CMake,加
set(CMAKE_BUILD_TYPE "Debug")并确认CMAKE_CXX_FLAGS_DEBUG不含-O2 - 避免链接时 LTO(
-flto),它会让cachegrind无法匹配源码行
cachegrind 输出里真正要看的不是总 Ir,而是 I1mr 和 LLmr
cachegrind 默认只打印摘要,但单元测试性能瓶颈往往藏在缓存未命中率里。运行后执行:
valgrind --tool=cachegrind --cachegrind-out-file=cg.out ./test_unit
然后看 cg_annotate cg.out 的头部摘要:
-
I1mr:一级指令缓存未命中率,>1% 就值得查(比如密集跳转、小函数反复调用) -
LLmr:最后一级缓存(LLC)数据未命中率,>5% 往往说明热点数据集远超 L3 容量,或访问模式不局部 - 别迷信
Ir(指令数)——它受编译器影响太大;D1mr(一级数据缓存未命中)对内存密集型断言(如 memcpy 比较大 buffer)更敏感
单元测试中 cachegrind 报告“无源码行”?检查 __attribute__((noinline)) 和宏展开
常见现象:cg_annotate cg.out test_unit.c 显示大量 ??? 行,或只标在 main() 或 assert 宏上。原因通常是:
- 断言宏(如
assert()、EXPECT_EQ)被展开成内联汇编或长表达式,cachegrind无法映射回原始行号 - 关键函数被编译器自动内联(即使
-O0下某些 trivial 函数仍可能内联) - 解决办法:给待分析的测试函数加
__attribute__((noinline)),例如:
void __attribute__((noinline)) test_string_parse() { ... }
- 把复杂断言拆成独立变量赋值 + 单独
assert,减少宏膨胀 - 用
cg_annotate --auto=yes cg.out让工具自动尝试匹配符号(比手动指定文件更可靠)
对比不同测试用例的缓存行为,用 --cachegrind-out-file 分离输出
单元测试套件通常有几十个 case,混在一起跑 cachegrind 会掩盖单个 case 的毛刺。正确做法是逐个运行并命名输出:
valgrind --tool=cachegrind --cachegrind-out-file=cg_case1.out ./test_unit --gtest_filter=TestCase1 valgrind --tool=cachegrind --cachegrind-out-file=cg_case2.out ./test_unit --gtest_filter=TestCase2
再分别 cg_annotate 对比 LLmr 和热点函数,能快速定位哪个 case 引入了非局部内存访问——比如某个 case 初始化了一个全局 hash 表,导致后续所有 case 的 LLC 命中率骤降。
注意:cachegrind 本身不支持多线程精确采样(线程切换会污染 cache 模拟),如果测试用例启了线程,结果中 LLmr 会虚高,此时应改用 callgrind 配合 --separate-threads=yes 查调用频次,而非缓存行为。











