google benchmark 介绍
在优化中,最常见的问题是某个优化点是否有效,尤其是某些函数级别的代码优化,比例不是很大(<10%),如何确定是性能波动,还是优化呢? google benchmark 是一个测试函数性能的框架,可以测试函数的性能,此外提供了对比工具,可以“科学”的做性能对比。
1 安装
# 下载 git clone https://github.com/google/benchmark.git && cd benchmark # 编译需要用到bazel,官方提供了cmake,但我使用报错 bazel build -c opt benchmark benchmark_main # 安装benchmark库 cp -a bazel-benchmark/include/benchmark /usr/local/include/ cp bazel-bin/libbenchmark_main.a bazel-bin/libbenchmark.a /usr/local/lib64 # 安装对比工具,在tools目录,需要python3 pip3 install scipy numpy cp -a gbench /usr/local/lib64/python3.6/site-packages/ cp compare.py /usr/local/bin/benchmark_compare.py
2 编写测试用例
dram_access_benchmark.cc
#include <cstring> #include <vector> #include <benchmark/benchmark.h> namespace { constexpr long buf_size = 12 * 1000 * 1000 * 1000L; // 12G; int fib(int n) { int v1 = 1; int v2 = 1; for (int i = 2; i < n; ++i) { int v = v1 + v2; v1 = v2; v2 = v; } return v2; } void locs_gen(std::vector<long> *locs) { for (int i = 0, max = locs->size(); i < max; ++i) { (*locs)[i] = random() % buf_size; } } char *buf = 0; void DoSetup(const benchmark::State& state) { if (!buf) { buf = new char[buf_size]; memset(buf, 0x9, buf_size/2); memset(buf + buf_size/2, 0x7, buf_size - buf_size/2); } } void DoTeardown(const benchmark::State& state) { } void BM_dram_access(benchmark::State &state) { std::vector<long> locs(state.range(0), 0); std::vector<long> regs(state.range(0), 0); long s = 100; long cnt = 0; for (auto _ : state) { locs_gen(&locs); #ifdef OOO for (int i = 0, max = locs.size(); i < max; ++i) { regs[i] = buf[locs[i]]; } for (int i = 0, max = regs.size(); i < max; ++i) { s += fib(regs[i]); } #else for (int i = 0, max = locs.size(); i < max; ++i) { s += fib(buf[locs[i]]); } #endif cnt++; } state.SetBytesProcessed(cnt * locs.size()); benchmark::DoNotOptimize(s); } BENCHMARK(BM_dram_access)->RangeMultiplier(2)->Range(4, 32)->Setup(DoSetup)->Teardown(DoTeardown); }
这里有几个关键点:
BM_dram_access
是测试函数,通过BENCHMARK注册,通常这就是全部了;- 可以给测试函数传递参数,这里
RangeMultiplier(2)->Range(4, 32)
的含义是传递4、8、16、32,依次是2的倍数; - 这里一次传递一个参数,每个参数生成一个测试用例,因为函数的性能可能和参数相关;
- 测试函数通过
state.range(0)
接收参数; - 通过
Setup(DoSetup)
初始化全局变量,注意的是每次测试都会调用一次DoSetup,但是测试框架可能调用多次测试函数(Warmup),所以如果只想全局调用一次这个函数,需要函数内部增加逻辑,保证只调用一次; - 测试框架度量一次执行的耗时,作为性能指标,此外可以自定义counter,来定义额外的性能指标,示例使用了一个内置counter;
benchmark::DoNotOptimize(s)
避免编译器把s相关指令优化掉,这点非常重要。
详细文档在这里,此外在源码 include/benchmark/benchmark.h
也可以找到一些例子。
3 编译运行
# 编译基准版本 g++ -g -Wall -O3 dram_access_benchmark.cc -o dram_access_benchmark -lbenchmark_main -lbenchmark -lpthread # 编译优化版本 g++ -DOOO -g -Wall -O3 dram_access_benchmark.cc -o dram_access_benchmark_opt -lbenchmark_main -lbenchmark -lpthread
-lbenchmark_main
使用默认的 main 函数。
dram_access_benchmark
可以直接运行,常用的参数有:
--benchmark_repelitions=<n>
运行的次数,次数(>30)越多,结果越准确;--benchmark_display_aggregates_only=true
多次运行时benchmark 会自动计算均值,中位数,这个参数只展示汇总值;--benchmark_counters_tabular=true
表格形式展示自定义counter。
4 优化效果对比
如前所述,如何检验优化真实有效呢?compare.py 实现了 Mann-Whitney U test(曼-惠特尼U检验),通过对比程序多次运行的结果,判断是真有优化,还是波动(这不同于单纯的比较多次运行的平均值,而是应用了U检验)。
详细文档在这里。
$ ./compare.py benchmarks ./dram_access_benmark ./dram_access_benchmark_opt --benchmark_repetitions=50 --benchmark_display_aggregates_only=true Benchmarks Time CPU Time Old Time New CPU Old CPU New ------------------------------------------------------------------------------------- BM_dram_access/32_pvalue 0.0000 0.0000 U Test, Repetions: 50 vs 50 BM_dram_access/32_mean -0.3534 -0.3536 2425 1568 2406 1555
这里运行次数为50,满足U检验。
- pvalue 为0,说明结果非常可信,通常小于0.05认为可信,越小越好;
- mean 是均值对比,-0.35表示性能改进了35%。
5 使用perf count
benchmark 可以测试、对比性能,但是无法给出性能差异上的原因,尤其在涉及硬件方面的优化时。google benchmark支持采集CPU的PMU,可以提供微架构方面的一些度量。google benchmark 通过libpfm采集PMU采集数据,以自定义counter的方式展示数据。
# 编译google benchmark添加libpfm支持,--define pfm=1 bazel build -c opt --define pfm=1 benchmark benchmark_main cp ./bazel-bin/external/libpfm/copy_libpfm/libpfm/lib/libpfm.so /usr/local/lib64 # 编译benchmark用例,-lpfm g++ -g -Wall -O3 dram_access_benchmark.cc -o dram_access_benchmark -lbenchmark_main -lbenchmark -lpthread -lpfm # 运行时指定pmu counter, 可以通过 perf list 查看可用的counter ./dram_access_benmark --benchmark_repetitions=50 --benchmark_display_aggregates_only=true --benchmark_perf_counters=cycles,instructions,mem_uops_retired.all_loads
google benchmark输出的是原始counter,可以在benchmark中获取原始counter值,计算复合counter,例如IPC。
void BM_dram_access(benchmark::State &state) { // ... for (auto _ : state) { // ... } // ipc = instructins/cycles auto cycles = state.counters["cycles"]; if (cycles) { auto ins = state.counters["instructions"]; state.counters["ipc"] = ins / cycles; } }
6 注意
代码的性能测试,因为并不是真实的线上环境,有些特别需要注意的地方。
- 编译优化:编译器会优化掉没用的代码,哪些代码会被优化掉,并不总是可以预料的,需要特别小心。