基准测试方法:防优化吞掉你的测量
Google Benchmark 思路、防编译器优化、尾延迟测量,以及机器人 pipeline 里怎么不被均值骗。

1. 测出来快 30%,线上无感
测出来「优化后快 30%」,合入主干后线上无感——多半是 benchmark 被编译器优化吞了,或者测的是 cache 热态而非 cold start。机器人 pipeline 里一帧 33ms 预算是 wall clock,不是 -O0 实验台。
Benchmark 是实验,不是 KPI——结论要能映射到一帧预算里的毫秒数。micro-bench 通过但 end-to-end bag replay 无提升时,瓶颈在 IO/锁/分配——benchmark 只证明 kernel 可行,不证明系统集成收益。上次点云 decode 优化 micro-bench 赢 35%,bag replay 无感,根因是 DDS publish 才是瓶颈——这类误判靠 PR 附 bag slice wall time 拦截。
2. 防止优化消除被测代码
DoNotOptimize 阻止 dead code elimination。写完 micro-benchmark 对照 -O0 与 -O3:若加速比离谱,先怀疑被优化空转,而不是急着改算法。别在循环外构造被测对象却只在循环内读常量——那是测 cache,不是测算法。
Google Benchmark 的 KeepRunning 循环足够长以摊平 timer 开销。Release 比 Debug 快 100×时,测的可能是空循环——先查 DCE,再改算法。
static void BM_decode(benchmark::State& state) {
std::vector<uint8_t> buf(state.range(0));
for (auto _ : state) {
benchmark::DoNotOptimize(decode_packet(buf.data(), buf.size()));
}
}
BENCHMARK(BM_decode)->Range(1<<10, 1<<16);3. 固定输入与隔离噪声
笔记本 benchmark 数字几乎不可复现。报告里写清 lscpu model 与 governor;机器人 onboard ARM 还要注明是否 big.LITTLE 核混跑。PR 附 p99 与 CPU governor;notebook 数字不能代替 onboard 结论。
- 关闭 CPU frequency scaling:
cpupower frequency-set -g performance。 - Pin 线程:
taskset -c 2 ./benchmark。 - 多次运行取中位数,报告 p95——机器人关心 tail latency。
- 预热:第一轮丢弃,避免 cold cache 误导;但若测的是「首帧延迟」,则 deliberately cold。
4. 对比要公平
改前后同一编译器、同一 -O2/LTO 开关、同一输入规模。benchmark 二进制与生产 node 的编译 flag 必须一致,否则 micro-bench 赢 30% 但 colcon release 无感。对 Eigen heavy 代码,-DEIGEN_NO_DEBUG 也要与 release 对齐,不然 debug 断言把 timer 撑爆。
对比「scalar vs SIMD」时,SIMD 版本别额外 alloc——把分配移出计时段。Eigen 表达式模板别在 benchmark 循环里重复临时对象。PR 里贴 cmake -LAH | rg CMAKE_CXX_FLAGS 与 benchmark 可执行文件的编译 flag diff。replay 同一 bag slice 三次,若方差大于 5%,先查 DDS 或日志 IO 有没有混进 loop。
5. 机器人场景测什么
- 点云 decode:bytes/ms,随 packet 大小 scaling。
- 锁竞争:多线程 publish 下 p99 延迟,不是 mean。
- 内存:
perf stat -e cache-misses看 L3 miss 是否随 map 规模线性涨。
Google Benchmark UseRealTime() 测 async pipeline;SetItemsProcessed 报 throughput。benchmark::Counter 报 bytes_processed。控制环看 p99/p999,mean 可以骗你。机器人 pipeline 的瓶颈 rarely 在单个 kernel,集成 replay 比 micro-bench 更接近真实预算。
6. 集成 replay 与 micro-bench 对齐
micro-bench 赢 30% 但 ros2 bag play 无感——瓶颈在 DDS/锁。benchmark PR 必须附 bag slice wall time 与 perf stat cycles 各一条。CI 存 JSON baseline,回归 >5% 红。report 同时写 mean 与 stddev;benchmark 与 bag replay 数字矛盾时以 bag 为准写 ticket。
./benchmark --benchmark_repetitions=5 --benchmark_report_aggregates_only=true
perf stat -e cycles,instructions,cache-misses ./benchmark --filter=BM_decode7. 失败症状
- Release 比 Debug 快 100×:测的是空循环。
- 改一行无关代码 benchmark 变 10%:输入或链接顺序变了。
- CI benchmark 波动 ±20%:未 pin CPU、并发 job 抢核。
冷 cache 用 PauseTiming 丢弃首轮。colcon 包 benchmark_main 链接进 CI,阈值 JSON 存 baseline 文件;回归超 8% 需 owner ack。benchmark 结论要能回答「这一帧预算里省了几毫秒」,否则优化不值得合入。
8. 案例:micro-bench 赢但 bag replay 无感
点云 decode micro-bench 优化后快 35%,但 ros2 bag play 端到端 wall time 无变化——瓶颈在 DDS publish 和 lock contention,不在 decode kernel。之后 PR 要求附 bag slice wall time,micro-bench 与 bag 数字矛盾时以 bag 为准。
9. 与 onboard 验证
绑核 taskset -c 2 写进脚本注释,防 laptop 误导。onboard ARM big.LITTLE 核混跑时 benchmark 数字波动大——报告里注明 governor 和核绑定策略。Benchmark 是实验,不是 KPI——结论要能映射到一帧预算里的毫秒数。
10. 决策:优化是否合入
合入前问:micro-bench 提升多少?bag replay 端到端提升多少?两者差距说明什么?若 micro-bench 赢 30% 但 bag 无感,优化可能不值得合入——或者需要同时优化 IO/锁路径。PR 附 p99 与编译 flag diff 表,reviewer 能判断数字是否可信。CI 存 JSON baseline,回归超阈值自动红,owner ack 才能 override。Benchmark 结论必须能回答「这一帧预算里省了几毫秒」。PR 附 p99 与 CPU governor,notebook 数字不能代替 onboard 结论,bag replay 与 micro-bench 数字矛盾时以 bag 为准写 ticket。绑核写进脚本注释,onboard ARM 注明 governor 和核绑定策略。CI 存 JSON baseline,回归超 8% 需 owner ack 才能 override。
相关
也可以看看
johan's blog