单元测试与性能测试都是我们实际进行项目开发中常见的需求,一个优秀的库能够帮助我们高效地完成上述需求,专注于项目本身逻辑的开发,而不是额外花费精力于单元测试库和性能测试库的编写。就
C++ 而言,最常用的单元测试和性能测试库应该就是 Google 开源的 GoogleTest
和 Benchmark 库了,二者源代码都可在 GitHub 获取。
本文中,我们以计算一块连续内存中的位 1 的数量为例(也就是实现位图
bitmap 的 countOnes
函数,这里为了简化,只考虑按字节连续存储的位图),来看看如何使用
GoogleTest 库进行单元测试以及如何使用 Benchmark
库进行性能测试。本文涉及的源码已经同步到 GitHub:UnpureRationalist/SWARBenchmark:
Performance comparison of popCount function. (github.com) 。
需要说明的是,本文仅仅是最基础的介绍,主要目的是说明如何使用
GoogleTest 和 Benchmark
这两个工具,不涉及它们的高级特性。需要使用这两个库高级功能的读者请参阅官方文档。
1. 目录组织 
使用 tree 命令查看目录组织情况如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $  tree --gitignore 
其中,third_party 目录是依赖的 GoogleTest 和 Benchmark
库所在路径,src 目录是项目源代码。项目使用
cmake 进行构建,需要在 CMakeLists.txt
文件中指定对 GoogleTest 和 Benchmark 的依赖,具体内容如下:
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 cmake_minimum_required (VERSION 3.10 )project (learnBenchmarkset (CMAKE_EXPORT_COMPILE_COMMANDS ON )set (BUILD_SHARED_LIBS OFF )set (CMAKE_CXX_STANDARD 17 )set (CMAKE_CXX_STANDARD_REQUIRED ON )add_subdirectory (third_party)find_package (OpenMP REQUIRED)set (SOURCESadd_executable ("swarBenchmark"  ${SOURCES}  src/benchmark.cpp)target_link_libraries ("swarBenchmark"  benchmark::benchmark OpenMP::OpenMP_CXX)enable_testing ()add_executable ("swarTest"  ${SOURCES}  src/test .cpp)target_link_libraries ("swarTest"  gtest gtest_main OpenMP::OpenMP_CXX)
项目只会生成两个可执行文件:
swarTest:单元测试可执行文件swarBenchmark:性能测试可执行文件 
2. countOnes 函数实现 
在本文中,共实现了 5 种 countOnes 函数,其中一个是 Redis
源码中采用的 SWAR 算法 + 手动循环展开。Redis
中的实现网上已经有很多介绍文章(可参考:https://ponder.work/2020/08/01/variable-precision-SWAR-algorithm/ 
),这里就简单介绍下朴素的实现算法:
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 long  long  basicPopcount (const  unsigned  char  *s, long  count)  long  long  bits = 0 ;for  (long  i = 0 ; i < count; ++i) {for  (int  j = 0 ; j < 8 ; ++j) {1  << j)) > 0 ;return  bits;long  long  bitAndPopcount (const  unsigned  char  *s, long  count)  long  long  bits = 0 ;for  (long  i = 0 ; i < count; ++i) {unsigned  char  c = s[i];while  (c) {1 ;return  bits;long  long  gccBuildinPopCount (const  unsigned  char  *s, long  count)  long  long  bits = 0 ;for  (long  i = 0 ; i < count; ++i) {return  bits;long  long  openmpBasicPopCount (const  unsigned  char  *s, long  count)  long  long  bits = 0 ;#pragma  omp parallel for reduction(+:bits) num_threads(2) for  (long  i = 0 ; i < count; ++i) {for  (int  j = 0 ; j < 8 ; ++j) {1  << j)) > 0 ;return  bits;
给出源码后,其实也没什么好解释的了,内部的实现不同点如下:
bits += (s[i] & (1 << j)) > 0;:朴素位运算c &= c - 1;:稍高级的位运算,可参考剑指
offer、力扣等了解原理bits += __builtin_popcount(s[i]);:使用 gcc
编译器内置函数#pragma omp parallel for reduction(+:bits) num_threads(2):使用
OpenMP 多线程加速朴素位运算 
3. 单元测试 
单元测试源码路径为 src/test.cpp,源码为:
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 #include  "genString.h"  #include  "gtest/gtest.h"  #include  "swar.h"  #include  <ctime>  #include  <random>  #include  <string>  class  CountOnesTest  : public  testing::Test {protected :CountOnesTest () = default ;CountOnesTest () override  = default ;void  doCountOnesTest (int  n)  generateRandomString (n);long  long  redisLen = redisPopcount ((void  *)str.c_str (), n);long  long  basicLen = basicPopcount ((const  unsigned  char  *)str.c_str (), n);long  long  bitAndLen = bitAndPopcount ((const  unsigned  char  *)str.c_str (), n);long  long  gccAndLen = gccBuildinPopCount ((const  unsigned  char  *)str.c_str (), n);long  long  openmpBasicLen = openmpBasicPopCount ((const  unsigned  char  *)str.c_str (), n);EXPECT_EQ (redisLen, basicLen);EXPECT_EQ (redisLen, bitAndLen);EXPECT_EQ (redisLen, gccAndLen);EXPECT_EQ (redisLen, openmpBasicLen);TEST_F (CountOnesTest, BasicTest) {int  n = 1000000 ;doCountOnesTest (n);TEST_F (CountOnesTest, StrongTest) {int  times = 1000 ;for  (int  i = 0 ; i < times; ++i) {int  n = (i + 1 ) * (i + 1 );doCountOnesTest (n);
单元测试的编写其实很简单,只需要自定义一个基础自 Test
的类:class CountOnesTest : public testing::Test,然后使用宏指定
test suite 名和 test case
名,并在内部编写单元测试逻辑即可,比如上面的:
1 2 3 4 TEST_F (CountOnesTest, BasicTest) {int  n = 1000000 ;doCountOnesTest (n);
CountOnesTest 是我们自定义的类名,BasicTest
是我们给测试用例取得名字。在内部调用 doCountOnesTest
函数进行单元测试。doCountOnesTest 函数内使用
EXPECT_EQ 宏,保证不同的 countOnes
实现得到的结果相同。
运行单元测试,我们能够得到下面的输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 $  ./swarTest  
如果单元测试失败,输出会进行提示,我们需要对源码进行修复以通过单元测试。
4. 性能测试 
性能测试源码路径为 src/benchmark.cpp,具体源码为:
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 61 62 63 64 65 66 67 68 69 70 71 72 73 #include  "genString.h"  #include  "swar.h"  #include  "gtest/gtest.h"  #include  <benchmark/benchmark.h>  #include  <string>  #include  <vector>  void  inputInit ()  int  numStr = 1000 ;int  strLen = 10000 ;reserve (numStr);for  (int  i = 0 ; i < numStr; ++i) {emplace_back (generateRandomString (strLen));static  void  BM_RedisPopcount (benchmark::State &state)  for  (auto  _ : state) {for  (const  std::string &str : inputStrings) {redisPopcount ((void  *)str.c_str (), str.size ());BENCHMARK (BM_RedisPopcount);static  void  BM_BasicPopCount (benchmark::State &state)  for  (auto  _ : state) {for  (const  std::string &str : inputStrings) {basicPopcount ((const  unsigned  char  *)str.c_str (), str.size ());BENCHMARK (BM_BasicPopCount);static  void  BM_BitAndPopCount (benchmark::State &state)  for  (auto  _ : state) {for  (const  std::string &str : inputStrings) {bitAndPopcount ((const  unsigned  char  *)str.c_str (), str.size ());BENCHMARK (BM_BitAndPopCount);static  void  BM_GccBuiltinPopCount (benchmark::State &state)  for  (auto  _ : state) {for  (const  std::string &str : inputStrings) {gccBuildinPopCount ((const  unsigned  char  *)str.c_str (), str.size ());BENCHMARK (BM_GccBuiltinPopCount);static  void  BM_OpenmpBasicPopCount (benchmark::State &state)  for  (auto  _ : state) {for  (const  std::string &str : inputStrings) {openmpBasicPopCount ((const  unsigned  char  *)str.c_str (), str.size ());BENCHMARK (BM_OpenmpBasicPopCount);int  main (int  argc, char  **argv)  inputInit ();Initialize (&argc, argv);RunSpecifiedBenchmarks ();Shutdown ();return  0 ;
性能测试的编写也很容易,只需要定义一个函数,输入参数类型为
benchmark::State &state,然后在函数体内编写性能测试需要执行的函数,最后进行函数的注册,如上面的:
1 2 3 4 5 6 7 8 9 static  void  BM_RedisPopcount (benchmark::State &state)  for  (auto  _ : state) {for  (const  std::string &str : inputStrings) {redisPopcount ((void  *)str.c_str (), str.size ());BENCHMARK (BM_RedisPopcount);	
性能测试结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 $  ./swarBenchmark 
可以看出,Redis 的实现要远远快于其他实现,由此我们可以感受到 SWAR
算法和循环展开的魅力。而面试中希望面试者给出的解答
a & (a - 1)
在我们的测试场景下,性能反而不如最朴素的逐位按位与,gcc
的内置函数在我们的场景下同样不如朴素算法。使用 OpenMP 2
个线程加速朴素位运算算法后,能减少时间开销,但还是与 Redis
中的实现有一定差距。
5. 总结 
单元测试与性能测试都是实际工作和项目开发中常见的、甚至必须的需求,但是在学校中却较少接触到,导致了知识面的一些欠缺。本文以位图的
countOnes 函数的不同实现为例,介绍了 GoogleTest 和
Benchmark 的基本使用方法,补全程序员必备工具链的一小环。