26. 性能测试与基准测试:Google Benchmark 的使用、统计显著性、避免微基准测试陷阱

性能优化这件事,说白了就是「没有测量,就没有优化」。你觉得自己改得飞快,结果一跑 benchmark 反而更慢了——这种事我遇到过太多次了。所以今天咱们聊聊 Google Benchmark 这个库,以及怎么用它做靠谱的测试。

为什么需要基准测试?

你想想看,平时我们怎么判断代码快不快?

  • 「感觉上快了」—— 这基本不靠谱
  • 「用 std::chrono 手动计时」—— 嗯,比感觉强点,但误差大
  • 「跑一次看时间」—— 单次测量,噪声太多

基准测试要解决的核心问题就三个:可重复、可对比、可统计。Google Benchmark 就是干这个的。

核心原则:一次测量不是测量。你需要多次运行、统计均值、看方差,才能说「这个版本比那个版本快」。

Google Benchmark 快速上手

先来个最简单的例子。假设我们要测一个字符串拼接的性能:

#include <benchmark/benchmark.h>
#include <string>

// 用 += 拼接
static void BM_StringAppend(benchmark::State& state) {
    std::string str;
    for (auto _ : state) {
        str += "hello";
    }
}
BENCHMARK(BM_StringAppend);

// 用 reserve + += 拼接
static void BM_StringReserve(benchmark::State& state) {
    std::string str;
    str.reserve(100);
    for (auto _ : state) {
        str += "hello";
    }
}
BENCHMARK(BM_StringReserve);

BENCHMARK_MAIN();

编译运行后,你会看到类似这样的输出:

----------------------------------------------------------
Benchmark                Time             CPU   Iterations
----------------------------------------------------------
BM_StringAppend        12.3 ns         12.3 ns     56789012
BM_StringReserve       8.21 ns         8.21 ns     85234567

看到没?reserve 之后快了将近 30%。但别急着下结论——这只是单次运行的结果。

统计显著性:别被一次结果骗了

我曾经犯过一个错误:测了一次觉得新版本快了 5%,就兴冲冲地合入了代码。结果上线后性能反而下降了。为什么?因为那次测试的噪声刚好偏向新版本。

Google Benchmark 默认会帮你做多次迭代,但还不够。你需要关注几个关键指标:

指标 含义 我的建议
CPU time 实际 CPU 执行时间 主要看这个
Wall time 墙上时钟时间 受系统负载影响大
Iterations 迭代次数 越多越稳定
标准差 结果波动程度 超过 3% 就要警惕

小技巧:--benchmark_repetitions=10 跑 10 轮,然后看均值和标准差。如果两版代码的均值差小于 2 倍标准差,那这个「优化」可能只是噪声。

微基准测试的常见陷阱

嗯,这里要重点说说陷阱。我踩过的坑,你最好别踩。

陷阱一:编译器优化把测试代码优化掉了

看这个例子:

static void BM_DeadCode(benchmark::State& state) {
    int x = 0;
    for (auto _ : state) {
        x = x + 1;  // 编译器可能直接优化掉
    }
}
BENCHMARK(BM_DeadCode);

结果跑出来是 0 ns。为什么?因为 x 从来没被使用过,编译器直接把它删了。

解决方案:benchmark::DoNotOptimize() 告诉编译器「别动这个变量」。

static void BM_NoOptimize(benchmark::State& state) {
    int x = 0;
    for (auto _ : state) {
        x = x + 1;
        benchmark::DoNotOptimize(x);
    }
}
BENCHMARK(BM_NoOptimize);

陷阱二:测试数据太小,不具代表性

我曾经测一个排序算法,只用了 10 个元素。结果插入排序比快速排序还快——这能说明什么?说明测试数据选错了。

Google Benchmark 支持参数化测试:

static void BM_Sort(benchmark::State& state) {
    std::vector<int> data(state.range(0));
    std::iota(data.begin(), data.end(), 0);
    std::shuffle(data.begin(), data.end(), std::mt19937{});

    for (auto _ : state) {
        std::sort(data.begin(), data.end());
        benchmark::DoNotOptimize(data);
    }
}
BENCHMARK(BM_Sort)->Range(8, 8<<10);  // 从 8 到 8192

陷阱三:忽略预热和冷启动

第一次运行和第十次运行,性能可能差很多。因为缓存、分支预测器、CPU 频率都在变化。

Google Benchmark 默认会做预热,但如果你测的是「冷启动」场景,需要手动控制:

BENCHMARK(BM_MyFunc)->MinWarmUpTime(0);  // 不预热

知识体系图

下面这张图总结了基准测试的核心流程和注意事项:

基准测试核心流程 编写 Benchmark 设置测试参数 多次运行 ⚠ 常见陷阱 • 编译器优化掉测试代码 → 用 DoNotOptimize • 测试数据太小 → 用 Range 参数化 • 忽略预热 → 控制 MinWarmUpTime • 单次运行 → 多次重复 + 统计 • 忽略标准差 → 看变异系数 • 测试环境不一致 → 固定 CPU 频率 分析输出:均值、标准差、对比 结论:有统计显著性才叫优化

实战建议:怎么避免踩坑

我个人习惯在项目里放一个 bench/ 目录,专门放基准测试代码。每次做性能优化前,先跑一遍基准测试,把结果存下来。优化后再跑一遍,对比差异。

这里有几个我总结的实用原则:

  1. 每次只改一个变量——同时改了两处代码,你分不清哪个起了作用
  2. 跑三轮以上——第一轮预热,后两轮取均值
  3. 看 CPU time,别只看 wall time——系统调度会影响 wall time
  4. 差异小于 5% 时,别急着下结论——可能是噪声,多跑几轮看看

一个实用技巧:--benchmark_filter=BM_MyFunc 只跑特定的测试,节省时间。开发阶段我经常这么干。

总结一下

基准测试不是跑一次就完事。它是个「测量-分析-改进-再测量」的循环。Google Benchmark 给了你工具,但怎么用好它,还得靠经验。

我记得刚学性能优化那会儿,总觉得自己改的代码肯定更快。后来被 benchmark 打脸打多了,就学乖了——先测再说,数据说话。

嗯,今天就聊到这儿。记住:没有统计显著性的优化,都是自我安慰

公众号:蓝海资料掘金营,微信 deep3321