Google Benchmark 性能测试分析工具

警告
本文最后更新于 2024-01-20,文中内容可能已过时。

0. 简介

作为一个程序而言,benchmark是非常关键的一个衡量指标,无论是程序算法的指标还是程序运行性能的指标,这些我们都可以去完成衡量。对于性能衡量而言google benchmark无疑是一个比较好的选择。



性能测试工具对比

1. google benchmark安装

google benchmark 下载地址

编译安装:

登录 linux环境,执行以下命令,进行编译安装:

1
2
3
4
5
6
7
8
9
git clone https://github.com/google/benchmark.git
cd benchmark
git clone https://github.com/google/googletest.git
mkdir build
cd build
cmake .. -DCMAKE_BUILD_TYPE=RELEASE
make -j4
# 如果想全局安装就接着运行下面的命令
sudo make install

2. 代码编写

创建一个C++源文件,并编写包含基准测试函数的代码。例如,创建一个名为benchmark_example.cpp的文件,并编写如下内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <benchmark/benchmark.h>

static void BM_MyFunction(benchmark::State& state) {
    // 在这里编写您要测试的代码
    for (auto _ : state) {
        // 执行您的代码
    }
}

BENCHMARK(BM_MyFunction);

BENCHMARK_MAIN();

在上述示例中,BM_MyFunction是您要测试的函数。

然后我们可以使用C++编译器编译您的代码,并链接Google Benchmark库。

1
g++ benchmark_example.cpp -o benchmark_example -lbenchmark -lpthread

如果是cmakelist,则可以使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread")
# benchmark依赖thread线程库
add_library(benchmark STATIC IMPORTED)
set_property(TARGET benchmark PROPERTY IMPORTED_LOCATION /usr/local/lib/libbenchmark.a)

add_executable(demo demo.cpp)
target_link_libraries(demo
    benchmark
)

install(TARGETS
    demo
    DESTINATION "bin/"
)

2.1 基础代码调用测试

我们可以看到每一个benchmark测试用例都是一个类型为std::function的函数,其中benchmark::State&负责测试的运行及额外参数的传递。

测试用例编写完成后,我们需要使用BENCHMARK()将我们的测试用例注册进benchmark,这样程序运行时才会执行我们的测试。

最后是用BENCHMARK_MAIN();替代直接编写的main函数,它会处理命令行参数并运行所有注册过的测试用例生成测试结果。

Example 1:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

#include <benchmark/benchmark.h>
#include <vector>
#include <array>

constexpr int len = 6;
std::vector<int> vec{1, 2, 3, 4, 5, 6};
std::array<int, len> array{1, 2, 3, 4, 5, 6};

// benchmark::State &state用于维护测试上下文信息,以及控制迭代次数
static void vector_test(benchmark::State &state)
{
    for (auto _ : state)
    {
        vec[0];
        vec[1];
        vec[2];
        vec[3];
        vec[4];
        vec[5];
    }
}

static void array_test(benchmark::State &state)
{
    for (auto _ : state)
    {
        array[0];
        array[1];
        array[2];
        array[3];
        array[4];
        array[5];
    }
}

// 注册测试用例
BENCHMARK(vector_test);
BENCHMARK(array_test);
// benchmark的主函数
BENCHMARK_MAIN();

结果格式如下:

1
2
3
4
5
6
Load Average: 0.43, 0.25, 0.10
------------------------------------------------------
Benchmark            Time             CPU   Iterations
------------------------------------------------------
vector_test       6.81 ns         6.81 ns    102373755
array_test        13.6 ns         13.6 ns     51227934
Example 2
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <benchmark/benchmark.h>
#include <array>

constexpr int len = 6;

// constexpr function具有inline属性,你应该把它放在头文件中
constexpr auto my_pow(const int i)
{
    return i * i;
}

// 使用operator[]读取元素,依次存入1-6的平方
static void bench_array_operator(benchmark::State& state)
{
    std::array<int, len> arr;
    constexpr int i = 1;
    for (auto _: state) {
        arr[0] = my_pow(i);
        arr[1] = my_pow(i+1);
        arr[2] = my_pow(i+2);
        arr[3] = my_pow(i+3);
        arr[4] = my_pow(i+4);
        arr[5] = my_pow(i+5);
    }
}
BENCHMARK(bench_array_operator);

// 使用at()读取元素,依次存入1-6的平方
static void bench_array_at(benchmark::State& state)
{
    std::array<int, len> arr;
    constexpr int i = 1;
    for (auto _: state) {
        arr.at(0) = my_pow(i);
        arr.at(1) = my_pow(i+1);
        arr.at(2) = my_pow(i+2);
        arr.at(3) = my_pow(i+3);
        arr.at(4) = my_pow(i+4);
        arr.at(5) = my_pow(i+5);
    }
}
BENCHMARK(bench_array_at);

// std::get<>(array)是一个constexpr function,它会返回容器内元素的引用,并在编译期检查数组的索引是否正确
static void bench_array_get(benchmark::State& state)
{
    std::array<int, len> arr;
    constexpr int i = 1;
    for (auto _: state) {
        std::get<0>(arr) = my_pow(i);
        std::get<1>(arr) = my_pow(i+1);
        std::get<2>(arr) = my_pow(i+2);
        std::get<3>(arr) = my_pow(i+3);
        std::get<4>(arr) = my_pow(i+4);
        std::get<5>(arr) = my_pow(i+5);
    }
}
BENCHMARK(bench_array_get);

BENCHMARK_MAIN();

我们可以看到每一个benchmark测试用例都是一个类型为std::function<void(benchmark::State&)>的函数,其中benchmark::State&负责测试的运行及额外参数的传递。

随后我们使用for (auto _: state) {}来运行需要测试的内容,state会选择合适的次数来运行循环,时间的计算从循环内的语句开始,所以我们可以选择像例子中一样在for循环之外初始化测试环境,然后在循环体内编写需要测试的代码。

测试用例编写完成后我们需要使用BENCHMARK(<function_name>);将我们的测试用例注册进benchmark,这样程序运行时才会执行我们的测试。

最后是用BENCHMARK_MAIN();替代直接编写的main函数,它会处理命令行参数并运行所有注册过的测试用例生成测试结果。

示例中大量使用了constexpt,这是为了能在编译期计算出需要的数值避免对测试产生太多噪音。

然后我们编译测试程序:

1
g++ -Wall -std=c++14 benchmark_example.cpp -pthread -lbenchmark

benchmark需要链接libbenchmark.so,所以需要指定-lbenchmark,此外还需要thread的支持,因为libstdc++不提供thread的底层实现,我们需要pthread。另外不建议使用-lpthread,官方表示会出现兼容问题,在我这测试也会出现链接错误。注意文件名一定要在-lbenchmark前面,否则编译会失败,具体参见:https://github.com/google/benchmark/issues/619

如果你是在Windows平台使用google/benchmark,那么你需要额外链接shlwapi.lib才能使benchmark正常编译和运行。详细信息在这里。

编译好程序后就可以运行测试了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
2024-01-20T15:56:26+08:00
Running ./benchmark_example_two
Run on (4 X 3700 MHz CPU s)
CPU Caches:
  L1 Data 32 KiB (x4)
  L1 Instruction 32 KiB (x4)
  L2 Unified 256 KiB (x4)
  L3 Unified 8192 KiB (x1)
Load Average: 0.36, 0.64, 0.82
***WARNING*** CPU scaling is enabled, the benchmark real time measurements may be noisy and will incur extra overhead.
---------------------------------------------------------------
Benchmark                     Time             CPU   Iterations
---------------------------------------------------------------
bench_array_operator       30.9 ns         30.6 ns     22700640
bench_array_at             31.1 ns         30.9 ns     22376913
bench_array_get            29.4 ns         29.4 ns     23760270

显示的警告信息表示在当前系统环境有一些噪音(例如其他在运行的程序)可能导致结果不太准确,并不影响我们的测试。

在Windows上通常没有上述警告,如果你需要在Linux平台上去除相关警告的话,请参考此处。

测试结果与预期基本相符,std::get最快,at()最慢。

2.2 传参调用测试

上面的测试用例都只接受一个benchmark::State&类型的参数,所以我们可以使用BENCHMARK宏生成的对象的Arg方法来完成参数的传递。

传递进来的参数会被放入state对象内部存储,通过range方法获取,调用时的参数0是传入参数的需要,对应第一个参数。

举个例子,假如我们需要实现一个队列,现在有ring buffer和linked list两种实现可选,现在我们要测试两种方案在不同情况下的性能表现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 必要的数据结构
#include <benchmark/benchmark.h>
#include "ring.h"
#include "linked_ring.h"

// ring buffer的测试
static void bench_array_ring_insert_int_10(benchmark::State& state)
{
    auto ring = ArrayRing<int>(10);
    for (auto _: state) {
        for (int i = 1; i <= 10; ++i) {
            ring.insert(i);
        }
        state.PauseTiming(); // 暂停计时
        ring.clear();
        state.ResumeTiming(); // 恢复计时
    }
}
BENCHMARK(bench_array_ring_insert_int_10);

// linked list的测试
static void bench_linked_queue_insert_int_10(benchmark::State &state)
{
    auto ring = LinkedRing<int>{};
    for (auto _:state) {
        for (int i = 0; i < 10; ++i) {
            ring.insert(i);
        }
        state.PauseTiming();
        ring.clear();
        state.ResumeTiming();
    }
}
BENCHMARK(bench_linked_queue_insert_int_10);
// 还有针对删除的测试,以及针对string的测试,都是高度重复的代码,这里不再罗列

很显然,上面的测试除了被测试类型和插入的数据量之外没有任何区别,如果可以通过传入参数进行控制的话就可以少写大量重复的代码。

编写重复的代码是浪费时间,而且往往意味着你在做一件蠢事,google的工程师们当然早就注意到了这一点。虽然测试用例只能接受一个benchmark::State&类型的参数,但我们可以将参数传递给state对象,然后在测试用例中获取:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
static void bench_array_ring_insert_int(benchmark::State& state)
{
    auto length = state.range(0);
    auto ring = ArrayRing<int>(length);
    for (auto _: state) {
        for (int i = 1; i <= length; ++i) {
            ring.insert(i);
        }
        state.PauseTiming();
        ring.clear();
        state.ResumeTiming();
    }
}
BENCHMARK(bench_array_ring_insert_int)->Arg(10);

上面的例子展示了如何传递和获取参数:

  • 传递参数使用BENCHMARK宏生成的对象的Arg方法
  • 传递进来的参数会被放入state对象内部存储,通过range方法获取,调用时的参数0是传入参数的需要,对应第一个参数

Arg方法一次只能传递一个参数,那如果一次想要传递多个参数呢?也很简单:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
static void bench_array_ring_insert_int(benchmark::State& state)
{
    auto ring = ArrayRing<int>(state.range(0));
    for (auto _: state) {
        for (int i = 1; i <= state.range(1); ++i) {
            ring.insert(i);
        }
        state.PauseTiming();
        ring.clear();
        state.ResumeTiming();
    }
}
BENCHMARK(bench_array_ring_insert_int)->Args({10, 10});

上面的例子没什么实际意义,只是为了展示如何传递多个参数,Args方法接受一个vector对象,所以我们可以使用c++11提供的大括号初始化器简化代码,获取参数依然通过state.range方法,1对应传递进来的第二个参数。

有一点值得注意,参数传递只能接受整数,如果你希望使用其他类型的附加参数,就需要另外想些办法了。

2.3 简化多个类似测试用例的生成功

向测试用例传递参数的最终目的是为了在不编写重复代码的情况下生成多个测试用例,在知道了如何传递参数后你可能会这么写:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
static void bench_array_ring_insert_int(benchmark::State& state)
{
    auto length = state.range(0);
    auto ring = ArrayRing<int>(length);
    for (auto _: state) {
        for (int i = 1; i <= length; ++i) {
            ring.insert(i);
        }
        state.PauseTiming();
        ring.clear();
        state.ResumeTiming();
    }
}
// 下面我们生成测试插入10,100,1000次的测试用例
BENCHMARK(bench_array_ring_insert_int)->Arg(10);
BENCHMARK(bench_array_ring_insert_int)->Arg(100);
BENCHMARK(bench_array_ring_insert_int)->Arg(1000);
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
2024-01-20T15:56:26+08:00
Running ./benchmark_example_two
Run on (4 X 3700 MHz CPU s)
CPU Caches:
  L1 Data 32 KiB (x4)
  L1 Instruction 32 KiB (x4)
  L2 Unified 256 KiB (x4)
  L3 Unified 8192 KiB (x1)
Load Average: 0.36, 0.64, 0.82
***WARNING*** CPU scaling is enabled, the benchmark real time measurements may be noisy and will incur extra overhead.
--------------------------------------------------------------------------------
Benchmark                                    Time              CPU      Iterations
----------------------------------------------------------------------------------
bench_array_ring_insert_int/10              584  ns           547 ns     1000000
bench_array_ring_insert_int/100             1357 ns           1367 ns     560000
bench_array_ring_insert_int/1000            9207 ns           9521 ns     64000

以上的代码虽然结果是正确的,但是仍然写了很多重复代码!

幸好ArgArgs会将我们的测试用例使用的参数进行注册以便产生用例名/参数的新测试用例,并且返回一个指向BENCHMARK宏生成对象的指针,换句话说,如果我们想要生成仅仅是参数不同的多个测试的话,只需要链式调用Arg和Args即可:

1
BENCHMARK(bench_array_ring_insert_int)->Arg(10)->Arg(100)->Arg(1000);

结果和上面一样。

但这还不是最优解,我们仍然重复调用了Arg方法,如果我们需要更多用例时就不得不又要做重复劳动了。

对此google benchmark也有解决办法:我们可以使用Range方法来自动生成一定范围内的参数。

先看看Range的原型:

1
BENCHMAEK(func)->Range(int64_t start, int64_t limit);

start表示参数范围起始的值,limit表示范围结束的值,Range所作用于的是一个_闭区间_。

但是如果我们这样改写代码,是会得到一个错误的测试结果:

1
BENCHMARK(bench_array_ring_insert_int)->Range(10, 1000);
1
2
3
4
5
6
7
--------------------------------------------------------------------------------
Benchmark                                   Time            CPU      Iterations
----------------------------------------------------------------------------------
bench_array_ring_insert_int/10              584  ns         625 ns     1000000
bench_array_ring_insert_int/64              1042 ns         1029 ns    896000
bench_array_ring_insert_int/512             4948  ns        5313 ns    100000
bench_array_ring_insert_int/1000            9221 ns         8545 ns     89600

为什么会这样呢?那是因为Range默认除了start和limit,中间的其余参数都会是某一个基底(base)的幂,基地默认为8,所以我们会看到64和512,它们分别是8的平方和立方。

想要改变这一行为也很简单,只要重新设置基底即可,通过使用RangeMultiplier方法:

1
BENCHMARK(bench_array_ring_insert_int)->RangeMultiplier(10)->Range(10, 1000);

现在结果恢复如初了。

使用Ranges可以处理多个参数的情况:

1
BENCHMARK(func)->RangeMultiplier(10)->Ranges({{10, 1000}, {128 256}});

第一个范围指定了测试用例的第一个传入参数的范围,而第二个范围指定了第二个传入参数可能的值(注意这里不是范围了)。

与下面的代码等价:

1
2
3
4
5
6
BENCHMARK(func)->Args({10, 128})
               ->Args({100, 128})
               ->Args({1000, 128})
               ->Args({10, 256})
               ->Args({100, 256})
               ->Args({1000, 256})

实际上就是用生成的第一个参数的范围于后面指定内容的参数做了一个笛卡尔积。

2.4 使用参数生成器

如果我想定制没有规律的更复杂的参数呢?这时就需要实现自定义的参数生成器了。

一个参数生成器的签名如下:

1
void CustomArguments(benchmark::internal::Benchmark* b);

我们在生成器中计算处参数,然后调用benchmark::internal::Benchmark对象的ArgArgs方法像上两节那样传入参数即可。

随后我们使用Apply方法把生成器应用到测试用例上:

1
BENCHMARK(func)->Apply(CustomArguments);

其实这一过程的原理并不复杂,我做个简单的解释:

  1. BENCHMARK宏产生的就是一个benchmark::internal::Benchmark对象然后返回了它的指针
  2. benchmark::internal::Benchmark对象传递参数需要使用ArgArgs等方法
  3. Apply方法会将参数中的函数应用在自身
  4. 我们在生成器里使用benchmark::internal::Benchmark对象的指针bArgs等方法传递参数,这时的b其实指向我们的测试用例

到此为止生成器是如何工作的已经一目了然了,当然从上面得出的结论,我们还可以让Apply做更多的事情。

下面看下Apply的具体使用:

1
2
3
4
5
6
7
8
9
// 这次我们生成100,200,...,1000的测试用例,用range是无法生成这些参数的
static void custom_args(benchmark::internal::Benchmark* b)
{
    for (int i = 100; i <= 1000; i += 100) {
        b->Arg(i);
    }
}

BENCHMARK(bench_array_ring_insert_int)->RangeMultiplier(10)->Apply(custom_args);

2.5 模板类的调用测试

如果针对每一种情况写测试函数,显然违反了DRY原则,因为除了vector的类型参数不同,其他代码几乎是完全一样的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <benchmark/benchmark.h>
#include <vector>
#include <array>

template <typename T, std::size_t length, bool is_reserve = true>
void bench_vector_reserve(benchmark::State& state)
{
    for (auto _ : state) {
        std::vector<T> container;
        if constexpr (is_reserve) {
            container.reserve(length);
        }
        for (std::size_t i = 0; i < length; ++i) {
            container.push_back(T{});
        }
    }
}
// BENCHMARK_TEMPLATE(bench_vector_reserve, std::string, 100);
// // benchmark的主函数
// BENCHMARK_MAIN();

非常的简单,我们通过length控制插入的元素个数;is_reserve则负责控制是否预分配内存,通过if constexpr可以生成reserve和不进行任何操作的两种代码(如果不熟悉c++17的if constexpr,推荐花两分钟看看这里)。

然后我们像往常一样定义一个测试用例:

1
BENCHMARK(bench_vector_reserve<std::string,100>);

可是等我们编译的时候却报错了!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ g++ test.cpp -lpthread -lbenchmark -lbenchmark_main

test.cpp:19:48: 错误:宏“BENCHMARK”传递了 2 个参数,但只需要 1   19 | BENCHMARK(bench_vector_reserve<std::string,100>);
      |                                                ^
In file included from a.cpp:1:
/usr/local/include/benchmark/benchmark.h:1146: 附注:macro "BENCHMARK" defined here
 1146 | #define BENCHMARK(n)                                     \
      |
test.cpp:19:1: 错误:‘BENCHMARK’不是一个类型名
   19 | BENCHMARK(bench_vector_reserve<std::string,100>);

原因是这样的,在编译器处理宏的时候实际上不会考虑c++语法,所以分割模板参数的逗号被识别成了分割宏参数的逗号,因此在宏处理器的眼里我们像是传了两个参数。这也说明了BENCHMARK是处理不了模板的。

不过别担心,Google早就想到这种情况了,所以提供了BENCHMARK_TEMPLATE宏,我们只需要把模板名字和需要的类型参数依次传给宏即可:

1
2
3
4
5
6
7
8
BENCHMARK_TEMPLATE(bench_vector_reserve, std::string, 100);
BENCHMARK_TEMPLATE(bench_vector_reserve, std::string, 1000);
BENCHMARK_TEMPLATE(bench_vector_reserve, std::string, 10000);
BENCHMARK_TEMPLATE(bench_vector_reserve, std::string, 100000);
BENCHMARK_TEMPLATE(bench_vector_reserve, std::string, 100, false);
BENCHMARK_TEMPLATE(bench_vector_reserve, std::string, 1000, false);
BENCHMARK_TEMPLATE(bench_vector_reserve, std::string, 10000, false);
BENCHMARK_TEMPLATE(bench_vector_reserve, std::string, 100000, false);

输出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
2024-01-20T19:12:57+08:00
Running ./benchmark_template
Run on (4 X 3700 MHz CPU s)
CPU Caches:
  L1 Data 32 KiB (x4)
  L1 Instruction 32 KiB (x4)
  L2 Unified 256 KiB (x4)
  L3 Unified 8192 KiB (x1)
Load Average: 1.67, 1.82, 1.39
***WARNING*** CPU scaling is enabled, the benchmark real time measurements may be noisy and will incur extra overhead.
-------------------------------------------------------------------------------------------
Benchmark                                                 Time             CPU   Iterations
-------------------------------------------------------------------------------------------
bench_vector_reserve<std::string, 100>                 2912 ns         2910 ns       239967
bench_vector_reserve<std::string, 1000>               27585 ns        27571 ns        25299
bench_vector_reserve<std::string, 10000>             275549 ns       275527 ns         2534
bench_vector_reserve<std::string, 100000>           3158585 ns      2818440 ns          253
bench_vector_reserve<std::string, 100, false>          7743 ns         7635 ns        89883
bench_vector_reserve<std::string, 1000, false>        54695 ns        54663 ns        12540
bench_vector_reserve<std::string, 10000, false>      671379 ns       671340 ns         1050
bench_vector_reserve<std::string, 100000, false>    8904492 ns      8903935 ns           79

2.6 定制测试参数

在上面的代码中,length参数其实是不必要的,所以代码可以这样改:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
template <typename T, bool is_reserve = true>
void bench_vector_reserve(benchmark::State& state)
{
	for (auto _ : state) {
		std::vector<T> container;
		if constexpr (is_reserve) {
            // 通过range方法获取传入的参数
			container.reserve(state.range(0));
		}
		for (std::size_t i = 0; i < state.range(0); ++i) {
			container.push_back(T{});
		}
	}
}

BENCHMARK_TEMPLATE(bench_vector_reserve, std::string)->RangeMultiplier(10)->Range(10, 10000 * 10);
BENCHMARK_TEMPLATE(bench_vector_reserve, std::string, false)->RangeMultiplier(10)->Range(10, 10000 * 10);

现在我们测试的元素数量是[10, 100, 1000, 10^4, 10^5]。

除此之外还有另一种叫“密集参数”的Ranges。google benchmark提供了DenseRange方法。

这个方法的原型如下:

1
DenseRange(int64_t start, int64_t end, int64_t step);

Ranges是累乘,而DenseRange是累加,因为累乘会导致几何级数的增长,在数轴上的分布越来越稀疏,累加则看上去像是均匀分布的,因此累加的参数生成器被叫做密集参数生成器

如果我们把测试用例这么改:

1
BENCHMARK_TEMPLATE(bench_vector_reserve, std::string)->DenseRange(1000, 100 * 100, 1000);

现在我们的length就是这样一个序列:[1000,2000,3000, ...,9000,10000]

关于自定义参数最后一个知识点是ArgsProduct。看名字就知道这是一个参数工厂。

1
ArgsProduct(const std::vector< std::vector<int64_t> >& arglists);

std::vector<int64_t>实际上就是一组参数,arglists就是多组参数的合集,他们之间会被求笛卡尔积,举个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
BENCHMARK(BM_test)->ArgsProduct({ {"a", "b", "c", "d"}, {1, 2, 3, 4} });

// 等价于下面的
BENCHMARK(BM_test)->Args({"a", 1})
                  ->Args({"a", 2})
                  ->Args({"a", 3})
                  ->Args({"a", 4})
                  ->Args({"b", 1})
                  ->Args({"b", 2})
                  ->Args({"b", 3})
                  ...
                  ->Args({"d", 3})
                  ->Args({"d", 4})
                  ```

我们可以看到参数工厂其实得自己手写所有参数,那如果我想配合工厂使用Ranges呢?

没问题,benchmark的开发者们早就想到了,所以提供了下面这些帮助函数:

1
2
benchmark::CreateRange(8, 128, /*multi=*/2)   // 生成:[8, 16, 32, 64, 128]
benchmark::CreateDenseRange(1, 6, /*step=*/1) // 生成:[1, 2, 3, 4, 5, 6]

如果换成我们的例子,就可以这样写:

1
2
3
4
5
6
BENCHMARK_TEMPLATE(bench_vector_reserve, std::string)->ArgsProduct({
    benchmark::CreateRange(10, 10000*10, 10)
});
BENCHMARK_TEMPLATE(bench_vector_reserve, std::string, false)->ArgsProduct({
    benchmark::CreateRange(10, 10000*10, 10)
});

借助仅仅两行代码我们就能生成数量可观的测试用例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2024-01-20T19:26:55+08:00
Running ./bm_template_2
Run on (4 X 3700 MHz CPU s)
CPU Caches:
  L1 Data 32 KiB (x4)
  L1 Instruction 32 KiB (x4)
  L2 Unified 256 KiB (x4)
  L3 Unified 8192 KiB (x1)
Load Average: 1.91, 2.05, 1.65
***WARNING*** CPU scaling is enabled, the benchmark real time measurements may be noisy and will incur extra overhead.
------------------------------------------------------------------------------------------
Benchmark                                                Time             CPU   Iterations
------------------------------------------------------------------------------------------
bench_vector_reserve<std::string>/10                   466 ns          466 ns      1505218
bench_vector_reserve<std::string>/100                 3549 ns         3548 ns       200461
bench_vector_reserve<std::string>/1000               34067 ns        34049 ns        20858
bench_vector_reserve<std::string>/10000             324499 ns       324370 ns         2125
bench_vector_reserve<std::string>/100000           3229254 ns      3227361 ns          219
bench_vector_reserve<std::string, false>/10           1604 ns         1603 ns       436414
bench_vector_reserve<std::string, false>/100          7707 ns         7705 ns        89743
bench_vector_reserve<std::string, false>/1000        57709 ns        57694 ns        12026
bench_vector_reserve<std::string, false>/10000      688582 ns       688283 ns         1008
bench_vector_reserve<std::string, false>/100000    9208480 ns      9205775 ns           75

当然,这只是一个类型参数,实际上我们还有另外两个类型需要测试。另外这是1.5.5新增的功能,如果你想尝鲜得先升级google benchmark。

通常做到上面那一步就足够了,然而在这里我们还有优化空间,因为如果我们把其他两个测试用的类型加上,代码是这样的,MyClass的定义后面会给出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
BENCHMARK_TEMPLATE(bench_vector_reserve, std::string)->ArgsProduct({
    benchmark::CreateRange(10, 10000*10, 10)
});
BENCHMARK_TEMPLATE(bench_vector_reserve, std::string, false)->ArgsProduct({
    benchmark::CreateRange(10, 10000*10, 10)
});
BENCHMARK_TEMPLATE(bench_vector_reserve, std::size_t)->ArgsProduct({
    benchmark::CreateRange(10, 10000*10, 10)
});
BENCHMARK_TEMPLATE(bench_vector_reserve, std::size_t, false)->ArgsProduct({
    benchmark::CreateRange(10, 10000*10, 10)
});
BENCHMARK_TEMPLATE(bench_vector_reserve, MyClass)->ArgsProduct({
    benchmark::CreateRange(10, 10000*10, 10)
});
BENCHMARK_TEMPLATE(bench_vector_reserve, MyClass, false)->ArgsProduct({
    benchmark::CreateRange(10, 10000*10, 10)
});

你看见了什么?没错,重复重复重复!我们又违背了DRY原则。

重复说不上什么十恶不赦,但能避免还是要避免的,所以我准备用宏来简化这些代码:

1
2
3
4
5
6
7
#define generate_test(type) \
	BENCHMARK_TEMPLATE(bench_vector_reserve, type)->ArgsProduct({benchmark::CreateRange(10, 100000, 10)}); \
	BENCHMARK_TEMPLATE(bench_vector_reserve, type, false)->ArgsProduct({benchmark::CreateRange(10, 100000, 10)});

generate_test(std::string);
generate_test(std::size_t);
generate_test(MyClass);

这下舒服多了。

接着来看我们的MyClass,我们的MyClass包含几个虚函数,禁止移动赋值,同时被刻意设计成了非平凡复制,这样的类型可以说是绕过了标准库容器设计的大部分优化措施,算是个妥妥的反面教材,希望你的项目里尽量不要出现这种东西:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class MyClass {
public:
	long i = 2L;
    MyClass() { i = 2L; }
	virtual ~MyClass() {}
	virtual long get() { return i; }
	MyClass& operator=(MyClass&&) = delete;
	MyClass(const MyClass& obj) {
		i = obj.i;
	}
	MyClass& operator=(const MyClass& obj) {
		i = obj.i;
	}
};

这个类其实就是针对内存分配器实现的,vector在重新进行内存分配后很可能靠移动语义或者memmove来移动数据,这两者将导致重新分配内存导致的性能损失变小,不利于我们观察vector的行为,所以我定制了这个类。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
2024-01-20T19:39:19+08:00
Running ./bm_template_3
Run on (4 X 3700 MHz CPU s)
CPU Caches:
  L1 Data 32 KiB (x4)
  L1 Instruction 32 KiB (x4)
  L2 Unified 256 KiB (x4)
  L3 Unified 8192 KiB (x1)
Load Average: 1.94, 1.42, 1.32
***WARNING*** CPU scaling is enabled, the benchmark real time measurements may be noisy and will incur extra overhead.
------------------------------------------------------------------------------------------
Benchmark                                                Time             CPU   Iterations
------------------------------------------------------------------------------------------
bench_vector_reserve<std::string>/10                   480 ns          480 ns      1399285
bench_vector_reserve<std::string>/100                 3446 ns         3445 ns       200045
bench_vector_reserve<std::string>/1000               34552 ns        33697 ns        20974
bench_vector_reserve<std::string>/10000             329746 ns       329407 ns         2123
bench_vector_reserve<std::string>/100000           3263084 ns      3262478 ns          215
bench_vector_reserve<std::string, false>/10           1603 ns         1602 ns       435234
bench_vector_reserve<std::string, false>/100          7871 ns         7870 ns        88586
bench_vector_reserve<std::string, false>/1000        58803 ns        58793 ns        11582
bench_vector_reserve<std::string, false>/10000      713880 ns       711949 ns          973
bench_vector_reserve<std::string, false>/100000    9387908 ns      9385776 ns           74
bench_vector_reserve<std::size_t>/10                   388 ns          387 ns      1800299
bench_vector_reserve<std::size_t>/100                 2421 ns         2421 ns       289565
bench_vector_reserve<std::size_t>/1000               23424 ns        23413 ns        29931
bench_vector_reserve<std::size_t>/10000             229590 ns       229543 ns         3081
bench_vector_reserve<std::size_t>/100000           2258210 ns      2257467 ns          309
bench_vector_reserve<std::size_t, false>/10           1331 ns         1329 ns       526682
bench_vector_reserve<std::size_t, false>/100          4098 ns         4094 ns       169896
bench_vector_reserve<std::size_t, false>/1000        26619 ns        26614 ns        26307
bench_vector_reserve<std::size_t, false>/10000      248256 ns       248125 ns         2864
bench_vector_reserve<std::size_t, false>/100000    2411262 ns      2410631 ns          286
bench_vector_reserve<MyClass>/10                       547 ns          547 ns      1264878
bench_vector_reserve<MyClass>/100                     4268 ns         4268 ns       163187
bench_vector_reserve<MyClass>/1000                   41305 ns        41292 ns        16957
bench_vector_reserve<MyClass>/10000                 408955 ns       408888 ns         1713
bench_vector_reserve<MyClass>/100000               4095353 ns      4093747 ns          170
bench_vector_reserve<MyClass, false>/10               1428 ns         1428 ns       487914
bench_vector_reserve<MyClass, false>/100              8052 ns         8050 ns        86405
bench_vector_reserve<MyClass, false>/1000            65784 ns        65772 ns        10679
bench_vector_reserve<MyClass, false>/10000          741154 ns       741025 ns          940
bench_vector_reserve<MyClass, false>/100000        6842572 ns      6839261 ns          102

2.7. 使用Benchmark接口

这里将待测试的函数注册为一个基准测试用例,并指定测试用例的名称和参数。该代码中使用了三种不同的注册方式:函数指针Lambda 函数带参数的函数指针。最后,使用 benchmark::RunSpecifiedBenchmarks 函数运行所有注册的基准测试用例,并使用 benchmark::Shutdown 函数释放资源。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <benchmark/benchmark.h>
#include <chrono>
#include <thread>

void BM_DemoSleep(benchmark::State& state) {
  for (auto _ : state){
    std::this_thread::sleep_for(std::chrono::nanoseconds(1000)); //待测试的代码
  }
}

void BM_DemoSleep1(benchmark::State& state, int id) {
  std::cout << "id:"<< id << std::endl;
  for (auto _ : state){
    std::this_thread::sleep_for(std::chrono::nanoseconds(1000));
  }
}

int main(int argc, char** argv) {
  benchmark::Initialize(&argc, argv); // 初始化Benchmark
  if (benchmark::ReportUnrecognizedArguments(argc, argv)) return 1;

  // 使用函数指针注册
  benchmark::RegisterBenchmark("BM_DemoSleep", &BM_DemoSleep);
  // 使用Lamba函数注册
  benchmark::RegisterBenchmark("BM_DemoSleep1", [](benchmark::State& state){
    for (auto _ : state){
      std::this_thread::sleep_for(std::chrono::nanoseconds(1000));
    }
  });

  // 使用带参数的函数指针注册
  int id = 10;
  benchmark::RegisterBenchmark("BM_DemoSleep2", &BM_DemoSleep1, id);

  benchmark::RunSpecifiedBenchmarks(); // 运行
  benchmark::Shutdown();
}

3. Ref:

[1]. https://mp.weixin.qq.com/s/hrKwlKj6i2twd_qNqaHyYg
[2]. Google Benchmark 用户手册
[3]. https://www.cnblogs.com/apocelipes/p/10348925.html

Buy me a coffee~
支付宝
微信
0%