性能基准测试:建立基线,选对工具

说到性能调优,我最常被问的问题是:“优化前和优化后到底快了多少?”
如果连这个都说不清楚,那调优就变成了拍脑袋。

所以,第二件事就是——建立性能基线。
说白了,就是给当前代码拍一张“快照”。之后每次改动,跟这张照片比一比,提升还是退步,一目了然。

为什么需要性能基线?

我几年前接手过一个 SDK 模块。上线前总觉得慢,但没数据。
后来硬着头皮跑了十遍测试,记下每遍耗时,取中位数。
从那时起,我就养成了习惯:任何调优,必须先拍照。

基线的作用就三点:

  • 量化起点,知道“现在有多慢”
  • 验证优化,判断“改完真快了”
  • 防止回退,避免“改了这个坏了那个”

基线建立四步法

  1. 选一个有代表性的场景 —— 别在空循环上测,要测真实数据流。
  2. 固定环境 —— CPU 频率、内存、负载、工具版本全锁死。
  3. 跑足够多次 —— 我个人习惯至少 20 次,取中位数或 P95。
  4. 记录指标 —— 不止耗时,还要看内存分配、GC 次数、吞吐量。

嗯,这里要注意:基线不是一成不变的。
SDK 升级、依赖库变动、测试机更换,都得重新拍一张。

常用工具:BenchmarkDotNet vs Google Benchmark

你现在主攻的是 .NET 还是 C++?
.NET 阵营就用 BenchmarkDotNet,C++ 就用 Google Benchmark。
两个都是工业级工具,我两边都用过。

特性BenchmarkDotNet (.NET)Google Benchmark (C++)
预热机制内置,自动跳过 JIT 抖动手动或自动,支持多次预热
统计输出中位数、标准差、P95 等均值、中位数、最小值等
内存诊断托管内存分配、GC 次数需结合 perf 或自定义
跨平台.NET 生态,Linux/Windows/macOS原生 C++,全平台
上手难度低,加个特性就行中等,需要 CMake 或构建

BenchmarkDotNet 示例

[MemoryDiagnoser]
public class MyBenchmark
{
    [Benchmark]
    public void OldWay()
    {
        // 旧实现
    }

    [Benchmark]
    public void NewWay()
    {
        // 新实现
    }
}

// 控制台启动:BenchmarkRunner.Run<MyBenchmark>();

这个框架会自动处理预热、迭代、冷启动。我一般跑完直接把结果导出成 markdown 贴到 PR 里。

Google Benchmark 示例

#include <benchmark/benchmark.h>

static void BM_OldWay(benchmark::State& state) {
    for (auto _ : state) {
        // 旧实现
    }
}
BENCHMARK(BM_OldWay);

static void BM_NewWay(benchmark::State& state) {
    for (auto _ : state) {
        // 新实现
    }
}
BENCHMARK(BM_NewWay);
BENCHMARK_MAIN();

我个人觉得 Google Benchmark 的优缺点都很明显:
跑起来快,但要自己管内存。曾经有次漏了预热,结果前 3 次迭代慢 30%,把我吓了一跳。

用例设计原则——避坑指南

我以前有个同事,设计了一个“空循环”基准测试,结果优化 100 倍。
实际上在生产代码里,那个循环从来不会被单独执行。这种基线就是废的。

设计测试用例时,我遵循这几个原则:

  • 贴近真实:输入数据的大小、分布、缓存命中概率尽量和线上对齐。
  • 隔离外部依赖:别把数据库、网络、IO 混进来,除非你要测的就是它们。
  • 排除 JIT/预热影响:跑前一定要充分预热,直到耗时稳定。
  • 多维度指标:不光看耗时,还要看内存分配、吞吐量、尾延迟。
⚠️ 我曾经踩过的坑:在 BenchmarkDotNet 里用 Stopwatch 手动计时,结果把框架的预热机制覆盖了。后来发现前后两次跑的结果波动 50%。想哭。

还有一个容易被忽略的:不要只看中位数。
如果你的 SDK 是实时音视频类的,P99 延迟更重要。

本章核心流程:基线建立到结果对比

下面这张图把整个过程串起来了——从定义场景到输出报告。

性能基准测试流程图 定义测试目标 选定测试工具 设计测试用例 运行并收集数据 建立性能基线 对比分析 & 输出报告 关键要点 • 场景必须真实 • 环境必须固定 • 预热充分 • 跑至少20次 • 记录多维度指标 常见误区 • 用空循环测性能 • 不预热直接跑 • 只看中位数 • 结果不保留原始数据
💡 我的个人小技巧:基线数据最好保留原始 json 文件。以后回查时,你可以直接重跑对比,不被原始摘要误导。

从一次真实压测说起

有个项目做图像处理 SDK,我一开始用 BenchmarkDotNet 跑了两组对比。
第一组没加 [MemoryDiagnoser],结果只看到耗时优化 15%。
加上后才发现,内存分配减少了 80%,GC 停顿直接从 120ms 降到 8ms。
这才是真正的收益。

你想想看,光看耗时,你可能错过最关键的 GC 优化。

所以,我的建议是:
基线不只是“快了多少”,而是“好在哪”。
选对工具,做对用例,记录全数据——这三把斧头砍下去,任何性能问题都别想逃。