性能测试与基准测试:用 Google Benchmark 给 C 函数“测速”

说实话,很多嵌入式工程师对性能测试的态度是——「大概跑一下,感觉还行就过了」。我以前也这样,直到有一次,我优化了一个算法,自认为快了 30%,结果放到实际产品里,系统反而变慢了。后来一查,是我用的计时方式被编译器优化掉了。嗯,从那以后,我就老老实实用专业的基准测试工具了。

今天要聊的 Google Benchmark,就是专门干这个的。它帮你精确测量函数的执行时间,还能自动处理编译器优化、多次运行取均值这些烦人的细节。说白了,就是让你知道你的代码到底跑多快,而不是「你以为它跑多快」。

为什么不用手写计时?

你可能想过:我直接用 clock_gettime() 或者 gettimeofday() 包一下不就行了?

嗯,理论上可以。但实际坑很多:

  • 编译器优化:你测的那个函数,可能被编译器直接优化掉了,结果测出来是 0 纳秒。
  • 系统噪声:后台进程、中断、调度延迟,都会污染你的测量结果。
  • 单次测量误差大:跑一次可能不准,需要多次取均值,还要剔除异常值。
  • 缺乏统计意义:你测了 100 次,但不知道方差多大,不知道结果是否可信。

Google Benchmark 把这些全包了。它自动运行多次,计算均值、标准差,还能帮你做回归测试——代码改了之后,性能是变好了还是变差了,一目了然。

一句话总结:Google Benchmark 是 C/C++ 性能测试的工业级标准,嵌入式、服务端、算法库都在用。

安装与集成

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(),它更强,相当于告诉编译器:「内存状态被修改了,别缓存任何东西」。我在测内存操作时经常用它。

我曾经踩过的坑:有一次我测一个 memcpy 的性能,忘了加 DoNotOptimize,结果测出来是 0.000001 ms。我还以为自己发现了超级优化技巧,后来才发现是编译器把整个 memcpy 优化掉了——因为它发现目标缓冲区没被使用。

多线程基准测试

嵌入式里多核越来越常见,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 核心知识体系 测试编写 • BENCHMARK 宏注册 • State 对象控制循环 • Arg / Range 参数化 • Threads 多线程测试 防优化技巧 • DoNotOptimize 标记 • ClobberMemory 屏障 • 每次迭代重置状态 • 避免死代码消除 结果分析 • 均值 / 标准差 • 迭代次数自动调整 • CPU vs 实时时间 • 回归比较 典型工作流程 编写测试 → 编译链接 → 运行基准 → 分析结果 → 优化代码 → 回归验证 常见陷阱 未加 DoNotOptimize | 状态未重置 | 单次测量 | 忽略系统噪声

进阶:自定义报告与比较

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 的核心用法就聊到这里。记住:性能测试不是一次性的,而是持续的过程。工具再好,也得坚持用才有价值。


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