性能基准测试:使用Google Benchmark编写微基准测试,统计显著性
性能优化这件事,最怕的就是「我感觉快了」。我见过太多团队,凭直觉改代码,结果上线后反而更慢。所以,基准测试不是可选项,而是必修课。
今天我们来聊聊微基准测试——用 Google Benchmark 这个库,把代码性能「称」出来。我会结合我自己的踩坑经验,告诉你怎样写测试才靠谱,怎样判断结果是不是真的「显著」。
为什么需要微基准测试?
你想想看,一个函数跑一次,可能只要几微秒。但系统里成千上万次调用,累积起来就是毫秒甚至秒级的差异。微基准测试就是把这些微小的差异放大,让你看得清清楚楚。
我在项目中遇到过一件事:一个字符串处理函数,肉眼看起来差不多,但用基准测试一跑,发现新版本比旧版本慢了 3 倍。原因?多了一次不必要的内存分配。这种问题,靠 code review 很难发现。
Google Benchmark 快速上手
Google Benchmark 是 Google 开源的 C++ 微基准测试库。它帮你处理了热身、迭代次数、统计计算这些琐事。你只需要写测试函数,剩下的交给它。
先看一个最简单的例子:
#include <benchmark/benchmark.h>
// 被测函数
int sum(int n) {
int s = 0;
for (int i = 0; i < n; ++i) s += i;
return s;
}
// 基准测试
static void BM_Sum(benchmark::State& state) {
int n = state.range(0);
for (auto _ : state) {
benchmark::DoNotOptimize(sum(n));
}
}
BENCHMARK(BM_Sum)->Arg(100)->Arg(1000)->Arg(10000);
BENCHMARK_MAIN();
编译运行后,你会看到类似这样的输出:
----------------------------------------------------------
Benchmark Time CPU Iterations
----------------------------------------------------------
BM_Sum/100 0.123 us 0.123 us 5680000
BM_Sum/1000 1.23 us 1.23 us 569000
BM_Sum/10000 12.3 us 12.3 us 56900
嗯,这里要注意:benchmark::DoNotOptimize 是防止编译器把计算结果优化掉。我刚开始写基准测试时,就吃过这个亏——编译器发现结果没用,直接把循环优化没了,测出来的时间几乎为零。
统计显著性:别被噪音骗了
跑一次基准测试,得到的时间是 1.23 us。再跑一次,变成 1.31 us。哪个是真的?
其实,两个都不一定是真的。因为系统有噪音——CPU 频率波动、缓存命中率、后台进程干扰,都会影响结果。所以我们需要统计显著性。
Google Benchmark 默认会跑多次迭代,计算均值和标准差。你看到输出里的 Iterations 列,就是它自动调整的迭代次数。它保证结果在统计上足够稳定。
我个人习惯,在对比两个版本的性能时,会看三点:
- 均值差异:新版比旧版快了多少?
- 标准差:如果标准差很大,说明结果不稳定,需要增加迭代次数。
- 置信区间:Google Benchmark 可以输出置信区间,帮你判断差异是否显著。
我曾经有一次优化,均值看起来快了 5%,但标准差也大了 4%。说白了,这个「优化」可能只是噪音。后来我增加了迭代次数,差异就消失了。
如何编写可靠的微基准测试
写基准测试,有几个坑你一定要避开。我列出来,你对照着检查:
- 编译器优化:被测代码的结果必须被使用,否则编译器可能直接删掉它。
- 热身不足:第一次运行代码时,缓存是冷的,分支预测器还没学习。需要跑几轮热身。
- 测试时间太短:如果一次测试只有几微秒,系统噪音的影响会很大。建议每次测试至少跑 1 毫秒。
- 环境不一致:不要在笔记本上一边充电一边跑测试,CPU 频率会变。最好固定 CPU 频率。
Google Benchmark 已经帮你处理了热身和迭代次数。但编译器优化这个坑,你得自己注意。看这个例子:
// 错误写法:结果被优化掉
static void BM_Bad(benchmark::State& state) {
int x = 0;
for (auto _ : state) {
x += 1; // 编译器可能直接优化掉
}
}
// 正确写法:使用 DoNotOptimize
static void BM_Good(benchmark::State& state) {
int x = 0;
for (auto _ : state) {
benchmark::DoNotOptimize(x += 1);
}
}
你想想看,如果编译器发现 x 从来没被用过,它会把整个循环删掉。测出来的时间就是 0。嗯,这个坑我踩过不止一次。
对比测试:A/B 比较
很多时候,我们不是想知道一个函数跑多快,而是想知道「新版本比旧版本快多少」。Google Benchmark 提供了 Compare 功能:
static void BM_VersionA(benchmark::State& state) {
for (auto _ : state) {
// 旧版本代码
}
}
static void BM_VersionB(benchmark::State& state) {
for (auto _ : state) {
// 新版本代码
}
}
BENCHMARK(BM_VersionA);
BENCHMARK(BM_VersionB);
// 在 main 中比较
int main(int argc, char** argv) {
::benchmark::Initialize(&argc, argv);
::benchmark::RunSpecifiedBenchmarks();
::benchmark::RegisterBenchmark("BM_VersionA", BM_VersionA);
::benchmark::RegisterBenchmark("BM_VersionB", BM_VersionB);
::benchmark::Compare("BM_VersionA", "BM_VersionB");
}
输出会直接告诉你:版本 B 比版本 A 快了多少百分比,以及这个差异是否统计显著。
参数化测试:覆盖不同输入规模
一个函数在不同输入规模下,性能表现可能完全不同。比如排序算法,小数据量时插入排序可能更快,大数据量时快速排序更优。
Google Benchmark 支持参数化测试:
static void BM_Sort(benchmark::State& state) {
int n = state.range(0);
std::vector<int> data(n);
for (auto _ : state) {
std::sort(data.begin(), data.end());
benchmark::DoNotOptimize(data);
}
}
BENCHMARK(BM_Sort)->RangeMultiplier(10)->Range(10, 100000);
这个测试会依次用 10、100、1000、10000、100000 个元素来跑。你可以看到性能随规模的变化曲线。
我个人习惯,在写基准测试时,至少覆盖三个数量级的输入规模。这样能发现一些隐藏的问题——比如某个算法在数据量小时很快,但数据量一大就崩了。
知识体系:微基准测试的核心逻辑
下面这张图,是我对微基准测试核心逻辑的总结。你可以把它当作一个检查清单:
实战:一个完整的例子
最后,我给你一个完整的实战例子。假设我们要比较两种字符串拼接方式:
#include <benchmark/benchmark.h>
#include <string>
#include <sstream>
// 方式A:使用 + 运算符
std::string concat_plus(const std::string& a, const std::string& b) {
return a + b;
}
// 方式B:使用 stringstream
std::string concat_stream(const std::string& a, const std::string& b) {
std::ostringstream oss;
oss << a << b;
return oss.str();
}
static void BM_ConcatPlus(benchmark::State& state) {
std::string a = "hello";
std::string b = "world";
for (auto _ : state) {
benchmark::DoNotOptimize(concat_plus(a, b));
}
}
static void BM_ConcatStream(benchmark::State& state) {
std::string a = "hello";
std::string b = "world";
for (auto _ : state) {
benchmark::DoNotOptimize(concat_stream(a, b));
}
}
BENCHMARK(BM_ConcatPlus);
BENCHMARK(BM_ConcatStream);
BENCHMARK_MAIN();
跑完之后,你大概率会发现 + 运算符比 stringstream 快很多。为什么?因为 stringstream 内部有格式化开销,而 + 直接操作内存。
- 微基准测试帮你量化性能,而不是凭感觉。
- Google Benchmark 自动处理热身、迭代、统计,你只需关注被测代码。
- 统计显著性比单次测量值更重要。看均值、标准差、置信区间。
- 参数化测试覆盖不同输入规模,避免「小数据快,大数据慢」的陷阱。
- 对比测试时,确保环境一致,避免编译器优化干扰。
好了,关于微基准测试,我就讲到这里。记住,性能优化不是玄学,而是科学。用数据说话,用基准测试验证。下次你改代码之前,先写个基准测试,跑一遍。改完之后再跑一遍。如果差异不显著,就别急着说「优化了」。
公众号:蓝海资料掘金营,微信deep3321