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);

}

这里有几个关键点:

  1. BM_dram_access 是测试函数,通过BENCHMARK注册,通常这就是全部了;
  2. 可以给测试函数传递参数,这里 RangeMultiplier(2)->Range(4, 32) 的含义是传递4、8、16、32,依次是2的倍数;
  3. 这里一次传递一个参数,每个参数生成一个测试用例,因为函数的性能可能和参数相关;
  4. 测试函数通过 state.range(0) 接收参数;
  5. 通过 Setup(DoSetup) 初始化全局变量,注意的是每次测试都会调用一次DoSetup,但是测试框架可能调用多次测试函数(Warmup),所以如果只想全局调用一次这个函数,需要函数内部增加逻辑,保证只调用一次;
  6. 测试框架度量一次执行的耗时,作为性能指标,此外可以自定义counter,来定义额外的性能指标,示例使用了一个内置counter;
  7. 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 可以直接运行,常用的参数有:

  1. --benchmark_repelitions=<n> 运行的次数,次数(>30)越多,结果越准确;
  2. --benchmark_display_aggregates_only=true 多次运行时benchmark 会自动计算均值,中位数,这个参数只展示汇总值;
  3. --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检验。

  1. pvalue 为0,说明结果非常可信,通常小于0.05认为可信,越小越好;
  2. 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 注意

代码的性能测试,因为并不是真实的线上环境,有些特别需要注意的地方。

  1. 编译优化:编译器会优化掉没用的代码,哪些代码会被优化掉,并不总是可以预料的,需要特别小心。

Author: zhengzhiyong

Created: 2024-02-25 日 07:57

Validate