免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

Google Benchmark:C++ 性能基准测试的开源神器,从零到实战

Google Benchmark:C++ 性能基准测试的开源神器,从零到实战 Google Benchmark 是 Google 开源的 C 基准测试Benchmark库专门用来科学地测量一段代码到底跑多快。它就像给代码配了一位专业计时裁判告诉你哪段代码快、快多少、稳定不稳定。开源地址GitHub - google/benchmark: A microbenchmark support library · GitHub许可证Apache-2.0可以放心用在商业项目里适用版本C11 及以上C17 体验更佳1. 为什么要用 Google Benchmark先看一个反面教材。假设你要比较两种排序谁快最常见的做法是#include chrono #include iostream #include vector #include algorithm #include random int main() { std::vectorint v(100000); std::mt19937 rng(42); // 固定随机种子保证可复现 std::generate(v.begin(), v.end(), rng); auto t0 std::chrono::high_resolution_clock::now(); // 记开始时间 std::sort(v.begin(), v.end()); // 被测代码 auto t1 std::chrono::high_resolution_clock::now(); // 记结束时间 std::cout 耗时: std::chrono::duration_caststd::chrono::microseconds(t1 - t0).count() 微秒\n; return 0; }这段代码有什么问题我们用体育比赛来类比问题 1只测一次运气成分大。就像短跑比赛只跑一趟就定冠军风一吹成绩就变了。CPU 频率波动、缓存冷热、系统后台任务都会影响单次耗时。问题 2没有热身。运动员上来直接跑容易受伤代码也一样——第一次调用时缓存还没加载、CPU 频率还没拉满测出来的往往是假慢。问题 3编译器可能作弊。如果你写完 std::sort 之后根本没用排序结果编译器会认为这段代码白写了直接优化掉结果测出来是 0 微秒不是真快是被删了。问题 4没有统计信息。真实性能应该有均值、方差、波动范围只有裸的耗时 123 微秒没有任何可信度。Google Benchmark 就是来解决这四件事的它会自动多次运行、自动热身、自动阻止编译器优化、自动输出均值/方差/百分位数还支持把不同参数组合批量跑完。2. Google Benchmark 的六大优点自动多次测量 统计每个基准默认跑足够多轮输出 mean均值、median中位数、std dev标准差、cv变异系数等指标你一眼就能看出结果稳不稳定。自动阻止编译器优化内置 DoNotOptimize() 和 ClobberMemory() 两个魔法函数让被测代码的副作用被真实保留杜绝假 0 秒。自动决定运行次数先试跑估算出合适的迭代次数让每个基准跑到毫秒级时长避免测了个寂寞。参数化基准支持 Range / Arg / ArgsProduct同一段代码自动换参数跑比如数据量 1 万、10 万、100 万各跑一次生成完整的性能曲线。丰富输出格式默认友好文本还能导出 JSON / CSV方便你画图或接入 CI持续集成做性能回归。接入简单、社区成熟单个头文件依赖少CMake 一键集成Google 自家和无数开源项目都在用质量有保证。3. 典型使用场景场景具体例子为什么非它不可性能回归测试每次代码提交后自动跑一遍核心函数耗时输出 JSON 可与上一次对比超阈值就报警数据结构对比std::unordered_map vs std::map vs absl::flat_hash_map需要在不同负载因子/容量下系统性对比优化效果验证加了 -O2、用了 SIMD单指令多数据后到底快多少需要排除噪音得到统计上可信的结论库调优自研内存池分配 vs malloc分配/释放这种微秒级操作最容易被编译器优化掉必须用专门工具教学/面试准备证明传引用比传值快、循环展开有效用真实数据说服别人而不是我觉得4. 环境准备三种方式安装方式 AvcpkgWindows 最省事# 1. 安装 vcpkg如果还没有 git clone https://github.com/microsoft/vcpkg.git cd vcpkg .\bootstrap-vcpkg.bat # 2. 安装 benchmark 库x64 版本 .\vcpkg install benchmark:x64-windows # 3. 记住输出里给的三方集成命令比如 .\vcpkg integrate install方式 BCMake FetchContent推荐零额外安装只需在 CMakeLists.txt 里声明CMake 会自动下载源码并编译cmake_minimum_required(VERSION 3.14) project(my_bench LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 自动从 GitHub 拉取 Google Benchmark 源码首次构建需联网 include(FetchContent) FetchContent_Declare( googlebenchmark GIT_REPOSITORY https://github.com/google/benchmark.git GIT_TAG v1.8.3 # 固定版本号保证可复现 ) FetchContent_MakeAvailable(googlebenchmark) # 关掉 benchmark 自带的测试加快构建 set(BENCHMARK_ENABLE_TESTING OFF CACHE BOOL FORCE) add_executable(my_bench bench_main.cpp) target_link_libraries(my_bench PRIVATE benchmark::benchmark)⚠️预警FetchContent_MakeAvailable(googlebenchmark) 一定要写在 BENCHMARK_ENABLE_TESTING OFF之后否则会多编译一坨测试代码慢得你想哭。方式 C源码编译Linux/macOS 通用git clone https://github.com/google/benchmark.git cd benchmark cmake -E make_directory build cmake -E chdir build cmake -DBENCHMARK_DOWNLOAD_DEPENDENCIESON -DCMAKE_BUILD_TYPERelease ../ cmake --build build --config Release sudo cmake --build build --config Release --target install5. 快速上手第一个基准测试5 步走我们测一个最常见的场景vector 尾插 vs 头插直观感受 Google Benchmark 的威力。步骤 1创建项目目录my_bench/ ├── CMakeLists.txt └── bench_main.cpp步骤 2写 CMakeLists.txt就用方式 B 的内容见上步骤 3写基准代码 bench_main.cpp#include benchmark/benchmark.h // Google Benchmark 唯一需要包含的头文件 #include vector #include deque // 被测函数 1往 vector 尾部插入 10 万个元素 // 参数 bench_state 是框架传入的状态对象用来汇报耗时和迭代次数 static void BM_Vector_PushBack(benchmark::State state) { for (auto _ : state) { // 框架自动决定循环多少次我们只管写一次逻辑 std::vectorint v; // 每次迭代都新建一个空 vector for (int i 0; i 100000; i) { v.push_back(i); // 尾插理论上很快内部扩容分摊后 O(1) } } } BENCHMARK(BM_Vector_PushBack); // 注册这个基准宏会生成一个入口函数 // 被测函数 2往 vector 头部插入同样数量的元素 static void BM_Vector_InsertFront(benchmark::State state) { for (auto _ : state) { std::vectorint v; for (int i 0; i 100000; i) { v.insert(v.begin(), i); // 头插每次都要把已有元素整体后移O(n) } } } BENCHMARK(BM_Vector_InsertFront); // 被测函数 3用 deque双端队列做头插对比一下 static void BM_Deque_PushFront(benchmark::State state) { for (auto _ : state) { std::dequeint dq; for (int i 0; i 100000; i) { dq.push_front(i); // deque 头插是 O(1)不需要移动已有元素 } } } BENCHMARK(BM_Deque_PushFront); // 程序入口固定写法照抄即可 BENCHMARK_MAIN();步骤 4编译运行# 在 my_bench 目录下 cmake -B build -DCMAKE_BUILD_TYPERelease # 一定要 Release cmake --build build --config Release ./build/my_bench # 运行基准程序⚠️预警新手第一大坑必须用 Release / -O2 以上优化级别编译否则编译器不优化、代码慢得离谱测出来的数据毫无意义。Debug 模式下 vector 头插能慢 100 倍以上。步骤 5看输出运行后你会看到类似这样的表格Run on (8 X 2496 MHz CPU s) CPU Caches: L1 Data 32 KiB (x4) L1 Instruction 32 KiB (x4) L2 Unified 512 KiB (x4) L3 Unified 8192 KiB (x1) Load Average: 0.35, 0.28, 0.24 ***WARNING*** CPU scaling is enabled, the benchmark real time measurements may be noisy and will incurs extra overhead. -------------------------------------------------------------- Benchmark Time CPU Iterations -------------------------------------------------------------- BM_Vector_PushBack 109 us 109 us 6408 BM_Vector_InsertFront 15323 us 15323 us 45 BM_Deque_PushFront 113 us 113 us 6256字段解读类比运动会的成绩单字段含义类比Time单次迭代的墙钟时间含等待单位默认纳秒/微秒看台上的挂钟CPU单次迭代实际占用 CPU 的时间运动员自己在跑的时间Iterations这次基准实际运行的迭代次数一共跑了多少趟us / ns自动选择的时间单位自动帮你换算单位结论一目了然头插比尾插慢了约 140 倍15323us vs 109usdeque 头插和 vector 尾插速度相当。这就是一份有说服力的性能证据。6. 进阶玩法6.1 阻止编译器优化掉你的代码新手第二大坑#include benchmark/benchmark.h // 这个函数会被优化成什么都不做吗试试就知道 static void BM_DoNothing(benchmark::State state) { for (auto _ : state) { int x 1 1; // 结果没人用编译器一优化就变成空气 } } BENCHMARK(BM_DoNothing);如果你直接跑很可能是 0 ns —— 不是真的快而是编译器把循环体整个删了。解决办法是把结果喂给框架static void BM_WithDoNotOptimize(benchmark::State state) { for (auto _ : state) { int x 1 1; benchmark::DoNotOptimize(x); // 告诉编译器这个值还有人要用不准删 } } BENCHMARK(BM_WithDoNotOptimize);benchmark::DoNotOptimize(x)相当于对编译器喊这个变量有副作用别动它但几乎没有实际开销是测量微秒级操作的关键。benchmark::ClobberMemory()更强告诉编译器内存可能被神秘修改了禁止它对内存读写做重排。测内存操作时经常用。⚠️预警被测量的函数如果有返回值一定要用 DoNotOptimize 接住否则整个计算都可能被编译器删除你会得到一份虚假的 0 耗时。6.2 参数化基准一条曲线代替三张表#include benchmark/benchmark.h #include vector #include algorithm #include random // 我们想测不同数据量下 std::sort 的表现 // state.range(0) 就是框架传入的参数这里表示数据量 static void BM_Sort(benchmark::State state) { std::vectorint v(state.range(0)); // 按参数创建数组 std::mt19937 rng(42); std::generate(v.begin(), v.end(), rng); for (auto _ : state) { std::vectorint copy v; // 每次迭代复制一份保证排序的都是同样的乱序数据 std::sort(copy.begin(), copy.end()); benchmark::DoNotOptimize(copy); // 防止编译器认为排序结果没用 } } // 从 110 (1024) 个元素到 120 (约 100 万) 个元素按 4 倍步长递增 BENCHMARK(BM_Sort)-RangeMultiplier(4)-Range(1 10, 1 20);运行后自动生成多条记录BM_Sort/1024 1.25 us 1.25 us 544790 BM_Sort/4096 5.89 us 5.89 us 118275 BM_Sort/16384 28.5 us 28.5 us 24362 BM_Sort/65536 133 us 133 us 5264 BM_Sort/262144 574 us 574 us 1221 BM_Sort/1048576 2718 us 2718 us 258Range(1 10, 1 20)起止值必须是 2 的幂或按倍数关系。RangeMultiplier(4)每一步乘 4所以是 1024 → 4096 → 16384 ...也可以 -Arg(500) -Arg(5000) 指定任意离散值或 -RangeMultiplier(10)-Range(1, 100000)。⚠️预警Range 的起止值如果不满足 2 的幂/倍数关系框架会报错或直接帮你调整最好都用 1 k 这种写法。6.3 多参数组合二维性能地图#include benchmark/benchmark.h #include vector #include unordered_map #include random // 同时传两个参数容器大小、插入条数 static void BM_UnorderedMapInsert(benchmark::State state) { int container_capacity state.range(0); // 参数 1预分配容量 int insert_count state.range(1); // 参数 2插入条数 for (auto _ : state) { std::unordered_mapint, int m; m.reserve(container_capacity); // 预分配减少 rehash for (int i 0; i insert_count; i) { m[i] i; } benchmark::DoNotOptimize(m); } } // 两两组合容量取 1000 或 100000插入条数取 1000 或 100000共 4 种情况 BENCHMARK(BM_UnorderedMapInsert) -Args({1000, 1000}) -Args({1000, 100000}) -Args({100000, 1000}) -Args({100000, 100000});输出里每个组合一行名称形如 BM_UnorderedMapInsert/1000/100000非常直观。6.4 用计数器报告额外指标有时光看耗时不够还想顺便报告内存占用等指标#include benchmark/benchmark.h #include vector static void BM_Vector_WithCounter(benchmark::State state) { for (auto _ : state) { std::vectorint v(100000, 1); benchmark::DoNotOptimize(v); state.counters[bytes_allocated] v.size() * sizeof(int); // 自定义指标 } } BENCHMARK(BM_Vector_WithCounter);输出会多一列 bytes_allocated。6.5 只跑你想跑的基准过滤./my_bench --benchmark_filterBM_Vector* # 只跑名字以 BM_Vector 开头的基准 ./my_bench --benchmark_filterBM_Sort/4096 # 只跑参数为 4096 的那一条 ./my_bench --benchmark_min_time1s # 每个基准至少跑 1 秒 ./my_bench --benchmark_repetitions10 # 每组重复 10 次输出每次的数据 ./my_bench --benchmark_formatjson # 导出 JSON 格式 ./my_bench --benchmark_formatcsv # 导出 CSV 格式⚠️预警重复测试--benchmark_repetitions时建议配合 --benchmark_report_aggregates_onlytrue让框架直接输出均值/方差而不是 10 行原始数据刷屏。7. 输出解析与格式导出7.1 JSON 导出接 CI / 画图最常用./my_bench --benchmark_formatjson result.jsonJSON 里每个基准对象长这样{ name: BM_Vector_InsertFront, run_type: aggregate, iterations: 45, real_time: 15323000, cpu_time: 15323000, time_unit: ns }你可以写脚本解析与上次跑的结果对比超过阈值就在 CI 里标红——这就是性能回归测试的雏形。7.2 解读方差cv 变异系数文本输出里可能还有 cv 字段coefficient of variation变异系数。类比变异系数 成绩的波动率。一般cv 值含义建议 1%结果非常稳定可信直接用1% ~ 10%有波动但可接受观察是否被后台任务影响 10%波动太大检查是否开了省电模式/有后台负载或增加运行时长8. 常见易错点⚠️ 集中预警⚠️ 用 Debug 模式测性能数据完全失真。必须 Release / -O2。⚠️ 不接住被测结果编译器把代码优化没了测出 0 ns 还以为是神级优化。用 DoNotOptimize。⚠️ 循环内重复初始化被测数据比如每次都 std::sort 同一个已排好序的数组——排序最快的情况就是已经有序你会测出假数据。每次迭代都要构造一份等价的输入如复制。⚠️ 用随机种子且不固定每次运行数据不同结果不可复现。固定种子如 std::mt19937 rng(42)。⚠️ 忽略热身自己手写计时没有热身Google Benchmark 已内置热身但如果你在 state 循环里写了第一次才初始化的懒加载逻辑会影响前几次迭代。⚠️ Range 参数不符合幂次关系框架可能直接调整导致你以为测的是 500实际跑的是 512。明确用 Arg 或符合规则的 Range。⚠️ 在多线程环境里测单线程代码后台任务会让 CPU 时间抖动。跑之前关掉重负载程序必要时用 --benchmark_repetitions 多次采样。⚠️ 把 benchmark 库和被测代码编成不同优化级别链接时以低优化级别的那份为准白测了。整个项目统一 Release。9. 对比表格9.1 Google Benchmark vs 手写 chrono 计时维度手写 chronoGoogle Benchmark自动多次运行❌ 要自己写循环✅ 内置自动热身❌ 要自己加✅ 内置防编译器优化❌ 容易踩坑✅ DoNotOptimize自动确定迭代次数❌ 要拍脑袋✅ 自适应统计指标均值/方差/百分位❌ 要自己算✅ 开箱即用参数化批量测试❌ 要自己写循环✅ Range/Args导出 JSON/CSV❌ 要自己写✅ 一行参数代码量最少也要 20 行核心 3 行9.2 Google Benchmark vs 相关开源工具工具类型与 Google Benchmark 的关系Catch2单元测试框架测功能对不对互补Catch2 验证正确性Benchmark 测速度GoogleTest单元测试框架互补常与 Benchmark 同项目使用perf / VTune系统级性能剖析器profiler互补Benchmark 告诉你快不快profiler 告诉你卡在哪Quick C Benchmark网页版简易评测弱化版只能做粗略对比不可靠、不可复现10. FAQ 速查表问题一句话答案Q1: 它和单元测试框架有什么区别单元测试如 Catch2/GoogleTest验证对不对Benchmark 测量快不快两者是互补关系Q2: 最低需要什么 C 标准C11 即可官方推荐 C17 体验最佳Q3: 为什么我测出来是 0 ns被测代码被编译器优化掉了用 benchmark::DoNotOptimize 接住结果Q4: 为什么要用 Release 编译Debug 模式不做优化测的是带训练服的成绩不是真实水平Q5: 怎么只测一个基准--benchmark_filter函数名*Q6: 结果波动大怎么办关掉后台负载加 --benchmark_repetitions10 多次采样必要时加 --benchmark_min_time5sQ7: 想导出数据画图--benchmark_formatjson 或 --benchmark_formatcsv再写脚本解析Q8: 商业项目能用吗可以Apache-2.0 许可证商用友好Q9: 参数化基准怎么传多个参数用 -Args({a, b}) 或 -ArgsProduct({{a1, a2}, {b1, b2}})Q10: 它在 CI 里怎么用输出 JSON → 脚本与基线对比 → 超阈值让流水线失败11. 总结Google Benchmark 是 C 性能工作流中性价比极高的一环对新手3 行代码就能得到一份科学、可信、可复现的性能数据比手写 chrono 靠谱得多对老手参数化基准 JSON 导出 CI 集成可以搭建完整的性能回归防线对团队Apache-2.0 商用友好和 Catch2/GoogleTest 完美互补是性能优化和算法选型的标准答案之一。上手建议先跑通本文第 5 节的 5 步示例再把自己项目里最核心的一段函数放进 for (auto _ : state)加一行 BENCHMARK(...)你就能拿到人生第一份可信的性能报告了。
返回列表