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); // 不预热
知识体系图
下面这张图总结了基准测试的核心流程和注意事项:
实战建议:怎么避免踩坑
我个人习惯在项目里放一个 bench/ 目录,专门放基准测试代码。每次做性能优化前,先跑一遍基准测试,把结果存下来。优化后再跑一遍,对比差异。
这里有几个我总结的实用原则:
- 每次只改一个变量——同时改了两处代码,你分不清哪个起了作用
- 跑三轮以上——第一轮预热,后两轮取均值
- 看 CPU time,别只看 wall time——系统调度会影响 wall time
- 差异小于 5% 时,别急着下结论——可能是噪声,多跑几轮看看
一个实用技巧:用 --benchmark_filter=BM_MyFunc 只跑特定的测试,节省时间。开发阶段我经常这么干。
总结一下
基准测试不是跑一次就完事。它是个「测量-分析-改进-再测量」的循环。Google Benchmark 给了你工具,但怎么用好它,还得靠经验。
我记得刚学性能优化那会儿,总觉得自己改的代码肯定更快。后来被 benchmark 打脸打多了,就学乖了——先测再说,数据说话。
嗯,今天就聊到这儿。记住:没有统计显著性的优化,都是自我安慰。