每周性能技巧 #75:如何做微基准测试

本节阅读量:

本文翻译自 Abseil 官网的 Performance Tip of the Week #75: How to microbenchmark

原文发布于 2024 年 1 月 19 日,作者 Chris Kennelly 和 Matt Kulukundis,更新于 2025 年 7 月 16 日。

微基准可以帮助我们快速比较局部实现,但也很容易误导。本篇讨论如何构造、解释并使用微基准,使它成为优化流程的一部分,而不是替代真实测量。

微基准适合回答什么

微基准擅长回答狭窄问题:

  • 两个小实现哪个更快。
  • 某个改动是否改变分配次数、指令数或 cache miss。
  • 某段代码的延迟和吞吐量特征是什么。
  • 编译器是否生成了预期形态的代码。

它不擅长回答端到端问题。真实生产环境有不同输入分布、不同缓存状态、不同共享资源竞争、不同编译参数和不同调用上下文。

避免测量优化器

编译器很擅长删除无用工作。如果 benchmark 的结果没有逃逸,编译器可能把整个计算优化掉。应该使用 benchmark 框架提供的工具,例如 benchmark::DoNotOptimizebenchmark::ClobberMemory,确保测量的是想测的代码。

1
2
3
4
5
6
7
void BM_StringCopy(benchmark::State& state) {
  std::string input = "some input";
  for (auto _ : state) {
    std::string copy = input;
    benchmark::DoNotOptimize(copy);
  }
}

同样,初始化工作应该尽量放在循环外,除非它就是要测量的一部分。把 setup 和热路径混在一起,会让结果难以解释。

延迟与吞吐量

同一段代码可以有不同的延迟和吞吐量。若每次迭代互不依赖,处理器可以并行执行多次迭代;若下一次迭代依赖上一次输出,就更接近测量单次操作延迟。

选择哪种方式取决于真实使用场景。循环中批量处理的代码更关心吞吐量;散落在复杂控制流中的小函数更可能关心延迟。

输入分布

输入选择会极大影响结果。排序数据、随机数据、小对象、大对象、命中缓存和不命中缓存都可能表现不同。

微基准应该反映真实工作负载,或明确说明自己测量的是某个极端情形。若不确定,可以测多组输入,并把差异当作理解代码行为的线索。

连接到生产

微基准是优化生命周期的早期工具。它可以快速失败,也可以建立初始信心。但最终仍要通过负载测试、生产 profile 或 A/B 实验验证。

如果微基准显示巨大收益但生产没有改善,可能说明热路径不同、输入分布不同、优化被其他瓶颈隐藏,或微基准本身没有测到真实成本。这个差异是学习机会。

结语

好的微基准小而明确,能回答具体问题,并且承认自己的边界。它应该帮助我们更快推进到真实验证,而不是替代真实验证。

上一篇:每周性能技巧 #74:不要把路灯扫到地毯下面

上一节

下一篇:每周性能技巧 #70:定义并衡量优化是否成功

下一节