性能测试与基准测试:用 Google Benchmark 给 C 函数“测速”
说实话,很多嵌入式工程师对性能测试的态度是——「大概跑一下,感觉还行就过了」。我以前也这样,直到有一次,我优化了一个算法,自认为快了 30%,结果放到实际产品里,系统反而变慢了。后来一查,是我用的计时方式被编译器优化掉了。嗯,从那以后,我就老老实实用专业的基准测试工具了。
今天要聊的 Google Benchmark,就是专门干这个的。它帮你精确测量函数的执行时间,还能自动处理编译器优化、多次运行取均值这些烦人的细节。说白了,就是让你知道你的代码到底跑多快,而不是「你以为它跑多快」。
为什么不用手写计时?
你可能想过:我直接用 clock_gettime() 或者 gettimeofday() 包一下不就行了?
嗯,理论上可以。但实际坑很多:
- 编译器优化:你测的那个函数,可能被编译器直接优化掉了,结果测出来是 0 纳秒。
- 系统噪声:后台进程、中断、调度延迟,都会污染你的测量结果。
- 单次测量误差大:跑一次可能不准,需要多次取均值,还要剔除异常值。
- 缺乏统计意义:你测了 100 次,但不知道方差多大,不知道结果是否可信。
Google Benchmark 把这些全包了。它自动运行多次,计算均值、标准差,还能帮你做回归测试——代码改了之后,性能是变好了还是变差了,一目了然。
安装与集成
Google Benchmark 是 CMake 项目,集成起来很简单。我个人习惯把它作为 Git Submodule 或者直接 FetchContent 拉下来。
# 用 CMake FetchContent 集成
include(FetchContent)
FetchContent_Declare(
benchmark
GIT_REPOSITORY https://github.com/google/benchmark.git
GIT_TAG v1.8.3
)
FetchContent_MakeAvailable(benchmark)
# 链接你的测试目标
target_link_libraries(my_benchmark PRIVATE benchmark::benchmark)
如果你在嵌入式 Linux 上交叉编译,记得关掉一些不必要的依赖,比如 BENCHMARK_ENABLE_TESTING=OFF。我在 ARM 板子上踩过这个坑——编译了半天,结果发现它在跑自己的单元测试,根本停不下来。
第一个基准测试:测一个排序函数
来看一个最简单的例子。假设我们要测一个冒泡排序的性能:
#include <benchmark/benchmark.h>
#include <vector>
#include <algorithm>
// 被测函数:冒泡排序
void bubble_sort(std::vector<int>& arr) {
for (size_t i = 0; i < arr.size(); ++i) {
for (size_t j = 0; j < arr.size() - i - 1; ++j) {
if (arr[j] > arr[j + 1]) {
std::swap(arr[j], arr[j + 1]);
}
}
}
}
// 基准测试用例
static void BM_BubbleSort(benchmark::State& state) {
// 准备数据:每次迭代都重新生成
std::vector<int> arr(state.range(0));
for (auto _ : state) {
// 重新填充随机数据
std::generate(arr.begin(), arr.end(), std::rand);
bubble_sort(arr);
}
}
BENCHMARK(BM_BubbleSort)->Arg(100)->Arg(1000)->Arg(10000);
BENCHMARK_MAIN();
这里有几个关键点:
benchmark::State是核心对象,它控制循环次数和计时。for (auto _ : state)这个循环,Google Benchmark 会自动决定跑多少次,直到统计稳定。state.range(0)获取参数,这里就是数组大小。Arg(100)指定不同的输入规模,自动生成多组测试。
编译运行后,你会看到类似这样的输出:
--------------------------------------------------------------
Benchmark Time CPU Iterations
--------------------------------------------------------------
BM_BubbleSort/100 0.002 ms 0.002 ms 345678
BM_BubbleSort/1000 0.201 ms 0.201 ms 3456
BM_BubbleSort/10000 20.12 ms 20.12 ms 345
看到没?它自动跑了成千上万次迭代,给出了平均时间。而且 Iterations 列告诉你它跑了多少次——数据量越大,迭代次数越少,因为单次时间长了。
参数化测试:一键测多种规模
刚才我们用 Arg() 手动指定了三个规模。如果规模很多,可以用 Range() 自动生成:
BENCHMARK(BM_BubbleSort)->Range(8, 8<<10); // 从 8 到 8192,按 2 的幂增长
这比手动写一堆 Arg() 方便多了。你想想看,如果我要测 10 种规模,手动写多累。
更高级的用法是自定义参数生成器。比如我想测 100、500、1000、5000、10000:
static void CustomArguments(benchmark::internal::Benchmark* b) {
for (int i = 100; i <= 10000; i *= 5) {
b->Arg(i);
}
}
BENCHMARK(BM_BubbleSort)->Apply(CustomArguments);
这个技巧在测不同数据分布时特别有用。比如测快速排序,我想分别测「随机数据」「有序数据」「逆序数据」,就可以用不同的参数组合。
避免编译器优化:DoNotOptimize 和 ClobberMemory
这是新手最容易踩的坑。看这个例子:
static void BM_Sum(benchmark::State& state) {
int arr[1000];
for (int i = 0; i < 1000; ++i) arr[i] = i;
int sum = 0;
for (auto _ : state) {
for (int i = 0; i < 1000; ++i) {
sum += arr[i];
}
}
}
BENCHMARK(BM_Sum);
你觉得这个测试结果可信吗?
不可信。因为编译器发现 sum 在循环结束后没有被使用,它可能直接把整个内层循环优化掉了。测出来的时间会异常小。
解决办法是用 benchmark::DoNotOptimize() 告诉编译器:「别动这个变量,它是有用的」。
static void BM_Sum(benchmark::State& state) {
int arr[1000];
for (int i = 0; i < 1000; ++i) arr[i] = i;
int sum = 0;
for (auto _ : state) {
for (int i = 0; i < 1000; ++i) {
sum += arr[i];
}
benchmark::DoNotOptimize(sum); // 防止优化掉 sum
}
}
还有一个 benchmark::ClobberMemory(),它更强,相当于告诉编译器:「内存状态被修改了,别缓存任何东西」。我在测内存操作时经常用它。
多线程基准测试
嵌入式里多核越来越常见,Google Benchmark 也支持多线程测试。用 Threads() 指定线程数:
static void BM_AtomicIncrement(benchmark::State& state) {
std::atomic<int> counter{0};
for (auto _ : state) {
counter.fetch_add(1, std::memory_order_relaxed);
}
}
BENCHMARK(BM_AtomicIncrement)->Threads(1)->Threads(2)->Threads(4);
输出会显示每个线程的平均时间,以及总吞吐量。这个在测锁竞争、原子操作时特别有用。
我记得有一次,我在一个四核芯片上优化一个共享队列。单线程测出来很快,但一上多线程就慢得离谱。用 Google Benchmark 一测,发现是锁的粒度太大,导致线程都在等锁。后来改成无锁队列,性能直接翻了三倍。
知识体系图
下面这张图总结了 Google Benchmark 的核心概念和流程:
进阶:自定义报告与比较
Google Benchmark 支持输出 JSON 或 CSV 格式,方便自动化分析:
./my_benchmark --benchmark_format=json > result.json
然后你可以写脚本比较两次运行的结果。比如代码优化前后,看每个测试项的变化百分比。我在 CI 里就是这么干的——每次提交代码,自动跑基准测试,如果性能下降超过 5%,就报错。
还有一个很实用的功能:--benchmark_filter。比如我只想跑名字包含 "Sort" 的测试:
./my_benchmark --benchmark_filter="Sort"
这在测试集很大的时候特别有用,不用每次都跑全部。
嵌入式环境下的注意事项
在嵌入式 Linux 或 RTOS 上跑 Google Benchmark,有几个点要留意:
- CPU 频率缩放:如果 CPU 有动态调频,测试结果会不稳定。建议锁频,或者用
--benchmark_min_time增加采样时间。 - 缓存预热:第一次跑的时候,缓存是冷的。Google Benchmark 默认会做预热迭代,但如果你发现结果波动大,可以手动增加预热次数。
- 中断干扰:在裸机或 RTOS 上,中断可能打断测试。我一般会在测试前关掉不必要的中断,或者把测试线程优先级提到最高。
--benchmark_repetitions=10 跑 10 轮,然后看标准差。如果标准差超过均值的 10%,说明环境噪声太大,需要排查。
总结
Google Benchmark 是我目前用过最顺手的 C/C++ 性能测试工具。它帮你把「测时间」这件事标准化、自动化了。你只需要关注测试逻辑本身,剩下的统计、防优化、多线程、参数化,它都帮你搞定。
我个人建议,每个嵌入式项目都应该建立一套基准测试集。不用多,核心算法、关键路径各写几个就行。每次改代码后跑一遍,性能是升是降,一目了然。这比靠感觉优化靠谱多了。
好了,关于 Google Benchmark 的核心用法就聊到这里。记住:性能测试不是一次性的,而是持续的过程。工具再好,也得坚持用才有价值。