首页 > 编程语言 > 详细

视频笔记 CppCon 2015: Chandler Carruth "Tuning C++: Benchmarks, and CPUs, and Compilers! Oh My!"

时间:2019-01-05 17:58:28      阅读:159      评论:0      收藏:0      [点我收藏+]

CppCon 2015: Chandler Carruth "Tuning C++: Benchmarks, and CPUs, and Compilers! Oh My!"

 
注:
  • 转载请说明出处 http://www.cnblogs.com/eagledai/
  • 视频中“废话”有点多,另外视频略微有点老,使用的编译器和OS都不一样,程序运行命令和结果都和笔记中的有部分不同。
 
10:50 Micro Benchmarks
用来benchmark一小段关键代码
 
12:00 Library on github
 
 
配置安装 google/benchmark  (Ubuntu):
git clone https://github.com/google/benchmark.git
cd benchmark
# If you want to build tests and don‘t use BENCHMARK_DOWNLOAD_DEPENDENCIES, then
# git clone https://github.com/google/googletest.git
mkdir build
cd build
cmake .. -DCMAKE_BUILD_TYPE=RELEASE
make

  # 安装到/usr/local/lib/ 和 usr/local/include/
  sudo make install

 

示例文件(与视频没有直接关联):example.cpp (from https://github.com/google/benchmark)
#include <benchmark/benchmark.h>
 
static void BM_StringCreation(benchmark::State& state) {
  for (auto _ : state)
    std::string empty_string;
}
// Register the function as a benchmarkBENCHMARK(BM_StringCreation);
 
// Define another benchmarkstatic void BM_StringCopy(benchmark::State& state) {
  std::string x = "hello";
  for (auto _ : state)
    std::string copy(x);
}
BENCHMARK(BM_StringCopy);
 
BENCHMARK_MAIN();

 

运行示例:
g++ -O3 -o example example.cpp -std=c++17 -lbenchmark -lpthread
./example
-------------------------------------------------------------
Benchmark                  Time             CPU   Iterations
-------------------------------------------------------------
BM_StringCreation      0.000 ns        0.000 ns   1000000000
BM_StringCopy           6.06 ns         6.05 ns    120777531

  

视频18:25 vector/bench.cpp
bench_create 仅仅创建 vector,当做 baseline
#include <benchmark/benchmark.h>
#include <vector>

static void bench_create(benchmark::State& state) {
  for (auto _ : state) {
    std::vector<int> v;
    (void)v;
  }
}
BENCHMARK(bench_create);

static void bench_push_back(benchmark::State& state) {
  for (auto _ : state) {
    std::vector<int> v;
    v.push_back(42);
  }
}
BENCHMARK(bench_push_back);

BENCHMARK_MAIN();

  运行结果:

Benchmark                Time             CPU   Iterations
-----------------------------------------------------------
bench_create         0.000 ns        0.000 ns   1000000000
bench_push_back       23.6 ns         23.5 ns     31392194

 

视频20:35 再加入 reserve,作为参照

static void bench_create(benchmark::State& state) {
  for (auto _ : state) {
    std::vector<int> v;
    (void)v;
  }
}
BENCHMARK(bench_create);

static void bench_reserve(benchmark::State& state) {
  for (auto _ : state) {
    std::vector<int> v;
    v.reserve(1);
  }
}
BENCHMARK(bench_reserve);

static void bench_push_back(benchmark::State& state) {
  for (auto _ : state) {
    std::vector<int> v;
    v.reserve(1);
    v.push_back(42);
  }
}
BENCHMARK(bench_push_back);

BENCHMARK_MAIN();

得到的结果和视频中的不一样!  

Benchmark                Time             CPU   Iterations
-----------------------------------------------------------
bench_create         0.000 ns        0.000 ns   1000000000
bench_reserve         24.0 ns         24.0 ns     30147044
bench_push_back       25.7 ns         25.6 ns     26591367

视频截图却是:

技术分享图片

记录(其实是采样)并生成数据。-g 表示生成调用关系图。注:可能需要sudo,程序bug导致。
perf record -g ./bench
perf report -g
 
视频 28:40
可惜的是,上面的第一句命令的调用关系是反的。需要给 -g 额外参数。
  • 下面额外的编译参数使用最小的代价(一个额外的寄存器)实现调用栈的动态定位,用来获得 call graph
  • ‘graph,0,5,caller‘
    • graph: %统计数字是相对于总体的,而不是相对于父函数的
    • 0.5: 是个filter,过滤掉数字小的函数
    • caller: 把 call graph 的关系倒过来,更符合普通人的习惯
    • 注:验证并没有效果,原因可能是 -O3优化太厉害,导致采样定位不准,换 -O2,可以数据较好看的 call graph,但程序运行要慢一些
 
依照视频,命令如下:
g++ -O3 -o bench vector/bench.cpp -std=c++17 -fno-omit-frame-pointer -lbenchmark -lpthread
sudo perf record -g ./bench
sudo perf report -g ‘graph,0.5,caller‘

实际采用O2, 甚至O1(根据需要),就可以得到比较好看好分析的 call graph。问题在于,-O2 较慢,只能参考,真正结果还是需要 -O3

g++ -O2 -o bench vector/bench.cpp -std=c++17 -fno-omit-frame-pointer -lbenchmark -lpthread
sudo perf record -g ./bench
sudo perf report -g

 

视频 39:00

技术分享图片

震惊:汇编显示,竟然没有生成 vector,全部被编译器优化掉了!编译器发现代码什么都没有做,干脆删掉。

视频 41:00 - 性能分析,怎么避免需要的代码不被优化
 
引入神奇的 escape 函数,关闭针对任意指针p的优化,适用于 clang, gcc, icc
static void escape(void *p) {
  asm volatile("" : : "g"(p) : "memory");
}
  • volatile: 告诉编译器,该汇编代码具有side effect,不要去优化汇编。一个例子,需要某段汇编生成确定8个字节的机器码,具有特殊用途,请编译器不要自作聪明去优化它
  • 该段代码意思是:接受地址 p 作为汇编的输入,可能把该地址的内容保存在内存的任何地方,也就是可能改变所有的内存,尽管事实上,什么也没去改变。其实是欺骗编译器,所以编译器会关闭优化
  • gcc/clang inline 汇编语法
    • string : CSV of outputs : CSV of inupts : list of clobbers
    • outputs: inline asm => C++
    • inputs: C++ => inline asm
    • clobber: what parts of the program it modified
static void clobber() {
  asm volatile("" : : : "memory");
}
  • 告诉编译器我们要往任何内存写数据,或者从任何内存读数据,但其实什么也没有做,没有代码产生
 把 escape 和 clobber 应用到代码:
static void bench_create(benchmark::State& state) {
  for (auto _ : state) {
    std::vector<int> v;
    escape(&v); // 需要保证向量地址可见
    (void)v;
  }
}
BENCHMARK(bench_create);

static void bench_reserve(benchmark::State& state) {
  for (auto _ : state) {
    std::vector<int> v;
    v.reserve(1);
    escape(v.data()); // 仅仅需要保证向量分配的内存可见,向量地址本身并不重要
  }
}
BENCHMARK(bench_reserve);

static void bench_push_back(benchmark::State& state) {
  for (auto _ : state) {
    std::vector<int> v;
    v.reserve(1);
    escape(v.data()); // 同上
    v.push_back(42);
    clobber(); // 表示我们需要读写任意地址,所以向量的 push_back 不会被优化掉
    // 很多人使用 no inline attribute 来实现同样的目的,但是有时不起作用(跨越函数边界来优化的时候),即使起作用的时候,
    // 我们还是希望需要 inline 的时候任然有 inline,函数的开销更大,不是真实的数字。
    // 问题:为什么有了 clobber,还需要上面的 escape?
    //      因为 v.reserve(1) 内部的内存分配对 clobber() 并不可见,除非使用 escape() 来注册过了
  }
}
BENCHMARK(bench_push_back);
简而言之
    编译器的模型里是一个连通图,每个节点是一块分配的内存区域,边就是指针,具有一个全局的假象的根指针,作为图的入口。新分配的内存(如上面最后一段代码中 v.reserve(1)分配的内存)并不自动加入这个图。escape(p) 保证 p 加入该连通图,相当于一个注册函数。

重新编译,profiling (注意,这里使用 O3 优化,perf 也不需要奇怪的参数,perf这几年应该对默认参数有所改进)  

g++ -O3 -o bench vector/bench.cpp -std=c++17 -fno-omit-frame-pointer -lbenchmark -lpthread
sudo perf record ./bench
sudo perf report

技术分享图片

技术分享图片

新的结果,
  • 上图中 bench_create 不再是0了!编译器不再把该代码段给优化没有了
  • 下图中 bench_create 比例较大,但是考虑 google/benchmark 给了它更多的 iterations,并不奇怪

视频 55:30 第二个示例
 
文件 fastmod/bench.cpp
#include <benchmark/benchmark.h>
#include <vector>
#include <random>

static void generate_arg_pairs(benchmark::internal::Benchmark *b) {
  for (int i = 1 << 4; i <= 1 << 10; i <<= 2) {
    for (int j : {32, 128, 224}) {
      b = b->ArgPair(i, j);
    }
  }
}

static void bench_fastmod(benchmark::State& state) {
  const int size = state.range(0);
  assert(size >= 16 && "Only support 16 intergers at a time!");
  const int ceil = state.range(1);
  std::vector<int> input, output;
  input.resize(size, 0);
  output.resize(size, 0);

  std::mt19937 rng;
  rng.seed(std::random_device()());
  std::uniform_int_distribution<int> dist(0, 255);
  for (int &i : input) {
    i = dist(rng);
  }

  for (auto _ : state) {
    for (int i = 0; i < size; ++i) {
      output[i] = input[i] >= ceil ? input[i] % ceil : input[i];
    }
  }
}

BENCHMARK(bench_fastmod)->Apply(generate_arg_pairs);
BENCHMARK_MAIN();
分析:这里是想评估特殊的 mod 操作(小于 ceil 时直接返回,大于时才进行 mod)。之所以要进行此改进,是因为 mod 的代价非常的大。
程序可以接受4 x 3的输入参数组合。输入参数1是输入/输出向量大小,2是 ceil 参数。输入向量填入随均匀分布的机数。
注:代码及命令均与视频有差异,已经根据OS,编译器 和 benchmark 版本进行改变。
 
g++ -O3 -o bench fastmod/bench.cpp -std=c++17 -fno-omit-frame-pointer -lbenchmark -lpthread
./bench
------------------------------------------------------------------
Benchmark                       Time             CPU   Iterations
------------------------------------------------------------------
bench_fastmod/16/32          38.5 ns         38.1 ns     19470616
bench_fastmod/16/128         20.6 ns         20.4 ns     24558625
bench_fastmod/16/224         13.1 ns         13.0 ns     51348951
bench_fastmod/64/32           158 ns          157 ns      5024877
bench_fastmod/64/128          106 ns          106 ns      6906793
bench_fastmod/64/224         60.8 ns         60.3 ns     10373068
bench_fastmod/256/32          701 ns          698 ns      1078959
bench_fastmod/256/128         365 ns          363 ns      1746600
bench_fastmod/256/224         211 ns          210 ns      3296786
bench_fastmod/1024/32        2799 ns         2788 ns       249359
bench_fastmod/1024/128       2516 ns         2508 ns       290976
bench_fastmod/1024/224       1763 ns         1758 ns       402332
分析:当 ceil 大的时候,很多mod操作被省略,程序变快!与预期相符。
 
sudo perf record ./bench
sudo perf report

技术分享图片

分析
  • 出乎预料,idiv 指令只占用很少的时间,而后面的 mov 却数字惊人。这是由于工具采样错误造成的。事实上,随后的指令时间应该基本上算到 idiv 身上
  • 视频中,idiv 出现两次,这是它所用的编译器进行的优化(一个循环内代码重复两次,循环次数减少一半)。我使用的编译器并没有生成这个优化。但不印象随后的手动优化:重复四次!
static void bench_fastmod(benchmark::State& state) {
   ...

  for (auto _ : state) {
    for (int i = 0; i < size; i += 4) {
#define mod(o)   output[i + o] = input[i + o] >= ceil ? input[i + o] % ceil : input[i + o];
      mod(0);
      mod(1);
      mod(2);
      mod(3);
    }
  }
}

------------------------------------------------------------------
Benchmark                       Time             CPU   Iterations
------------------------------------------------------------------
bench_fastmod/16/32          29.4 ns         29.4 ns     22096061
bench_fastmod/16/128         19.5 ns         19.5 ns     39823845
bench_fastmod/16/224         8.87 ns         8.86 ns     65449028
bench_fastmod/64/32           126 ns          126 ns      5407309
bench_fastmod/64/128         84.2 ns         84.1 ns      8509984
bench_fastmod/64/224         44.7 ns         44.7 ns     18966107
bench_fastmod/256/32          506 ns          506 ns      1399732
bench_fastmod/256/128         335 ns          334 ns      1933547
bench_fastmod/256/224         168 ns          168 ns      4090361
bench_fastmod/1024/32        2136 ns         2130 ns       359540
bench_fastmod/1024/128       1410 ns         1409 ns       492857
bench_fastmod/1024/224        645 ns          644 ns      1033980
分析:
  • 视频中并没有改善很多,但这里的结果改善还是很明显。可能的原因是视频中是从2到4,这里是从1到4
 
使用 filter 只抓取其中一组数据:
  sudo perf record ./bench --benchmark_filter=bench_fastmod/1024/128
  sudo perf report  

技术分享图片

分析:
  • 慢的原因是,很多跳转,造成 icache misses
  • 怎么修?我们知道大多数的数字比 ceil 要小,我们需要让编译器也明白这点!
#define UNLIKELY(x) __builtin_expect((bool)(x), 0)
...
#define mod(o)   output[i + o] = UNLIKELY(input[i + o] >= ceil) ? input[i + o] % ceil : input[i + o];

------------------------------------------------------------------
Benchmark                       Time             CPU   Iterations
------------------------------------------------------------------
bench_fastmod/16/32          34.5 ns         34.3 ns     20429463
bench_fastmod/16/128         20.3 ns         20.1 ns     37576458
bench_fastmod/16/224         12.7 ns         12.7 ns     91676108
bench_fastmod/64/32           138 ns          138 ns      5087081
bench_fastmod/64/128         79.2 ns         79.1 ns      7661134
bench_fastmod/64/224         36.8 ns         36.7 ns     18236189
bench_fastmod/256/32          519 ns          518 ns      1286709
bench_fastmod/256/128         332 ns          332 ns      2024787
bench_fastmod/256/224         144 ns          144 ns      5263624
bench_fastmod/1024/32        2440 ns         2437 ns       288749
bench_fastmod/1024/128       1360 ns         1357 ns       466528
bench_fastmod/1024/224        578 ns          577 ns      1159231
分析:
  • ceil=32 的变慢了一些,因为编译器没有向这个方向优化,而是向着相反的方向优化
  • ceil=224 的快了一些,
 
  sudo perf record ./bench --benchmark_filter=bench_fastmod/1024/224
  sudo perf report  

技术分享图片

分析:
  • 优化过的机器码认为跳转发生的概率低,基本可以不跳转,一路向下走
  • 下面(未显示部分)的 idiv 部分,尽管只有20%的概率命中,仍然占据了CPU计算的主要时间
 
视频 1:11:00 以上 benchmark 都没有 baseline,需要加回来
static void bench_mod(benchmark::State& state) {
  const int size = state.range(0);
  assert(size >= 16 && "Only support 16 intergers at a time!");
  const int ceil = state.range(1);
  std::vector<int> input, output;
  input.resize(size, 0);
  output.resize(size, 0);

  std::mt19937 rng;
  rng.seed(std::random_device()());
  std::uniform_int_distribution<int> dist(0, 255);
  for (int &i : input) {
    i = dist(rng);
  }

  for (auto _ : state) {
    for (int i = 0; i < size; ++i) {
      output[i] = input[i] % ceil;
    }
  }
}
BENCHMARK(bench_mod)->Apply(generate_arg_pairs);
 
g++ -O3 -o bench fastmod/bench.cpp -std=c++17 -fno-omit-frame-pointer -lbenchmark -lpthread
./bench
------------------------------------------------------------------
Benchmark                       Time             CPU   Iterations
------------------------------------------------------------------
bench_mod/16/32              38.8 ns         38.6 ns     18027258
bench_mod/16/128             41.9 ns         41.8 ns     17730982
bench_mod/16/224             40.6 ns         40.5 ns     17564358
bench_mod/64/32               163 ns          163 ns      4248128
bench_mod/64/128              161 ns          161 ns      4376018
bench_mod/64/224              161 ns          160 ns      4338062
bench_mod/256/32              632 ns          631 ns      1090116
bench_mod/256/128             636 ns          635 ns      1083590
bench_mod/256/224             644 ns          643 ns      1095520
bench_mod/1024/32            2585 ns         2579 ns       271321
bench_mod/1024/128           2606 ns         2602 ns       272193
bench_mod/1024/224           2589 ns         2583 ns       274946
bench_fastmod/16/32          37.2 ns         37.2 ns     18993096
bench_fastmod/16/128         23.6 ns         23.6 ns     30564935
bench_fastmod/16/224         12.1 ns         12.1 ns     75911463
bench_fastmod/64/32           151 ns          151 ns      4424330
bench_fastmod/64/128         84.2 ns         83.4 ns      8381393
bench_fastmod/64/224         38.5 ns         38.2 ns     22431970
bench_fastmod/256/32          499 ns          495 ns      1271434
bench_fastmod/256/128         341 ns          339 ns      1982904
bench_fastmod/256/224         135 ns          134 ns      5086716
bench_fastmod/1024/32        2064 ns         2053 ns       331504
bench_fastmod/1024/128       1360 ns         1353 ns       545493
bench_fastmod/1024/224        596 ns          594 ns      1119850
事实是,fastmod 确实快了一些,特别在 ceil 大的时候。即便在 ceil 小的时候,也有点点改善。
 

视频 1:14:30 - Q & A

视频时间
问题
回答
1:14:45  
perf 里的函数名字哪里来的?
程序的二进制代码里面就有
1:15:12
程序里随机数序列是否严格相同?
不,但都是均匀分布,而且效果看起来还不错,并没有给 benchmark 带来明显的差异
1:15:58
能不能用 std::chrone 获得时间
可以,但是这是 framework 所做的
1:16:51
为什么使用 omit frame pointer参数
其它的选项 dwarf unwinding,常常miss掉数据,难以信赖。frame pointer总能获得一致性的结果。
另一个LBR工具很不错,但有时会放大随机错误,导致不准确。
 
为什么不使用更准确的参数来测量,例如时钟周期,cache misses,指令数
Perf 里有很多特征,这里没有时间来细述
1:18:23
昨天有人聊到benchmark冷热不同的代码。google/benchmark为什么不跳过开始的冷代码?
有这么多次 iterations,不认为有必要跳过开始的那几轮
1:19:04
为什么不用 flag 关闭优化,就不用担心代码有移调?
我可以使用 flag 关闭优化,但问题也在于所有优化被关闭。我希望benchmark的是优化过的代码,更加接近真实生产场景,仅仅希望需要的优化被保留,而不优化掉不该的部分。
1:19:43
例子中采样有错误,为什么不增加采样速率?
 
采样速率增加不能解决这里的问题,这里的真实问题在于收集采样数据的方法。
 
 
向量指令benchmark偏差,Perf 工具有神么方法解决?
我不认为 Perf 里面有办法解决此问题,“我”自己的方法是, Perf以提供最原始的数据,再结合大脑的分析和修正,应该可以部分解决此问题。另外一个很好的工具是 Intel Architecture Code Analyzer。用户可以输入一段指令,它可以在Intel架构模型上解释这段指令该如何执行。有了这个数据作为参照,即使CPU有偏差,可以推断出对应的数据。“我”其实不信任任何工具帮我修正这些偏差,基本都要靠我自己的分析。
1:21:43
另外一种让编译器不要过于聪明的优化掉被测的不做实质工作代码的方法,是把结果输出到 volatile 变量中。相比 前面的 clobber 函数的方法,各有什么特点和不同?
是的,有多种方法,包含使用 volatile 变量的方法。过去使用 volatile 变量的方法有个问题,它太迟了,尤其是当benchmark的是一个内存操作序列的时候,例如一序列的push_back calls,这种 volatile 变量法可能会消除掉全部的 push_back 调用,直接保存一个常量到 volatile 变量里。
提问者:volatile 方法另一个问题是,你可以保证它可读(可见性),但不能保证它可写。
是的,“我”的代码更通用,你可以更精确的保留需要保留的代码。另一个把 clopper 放在尾部的原因,可以防止编译器进行 loop computation analysis,就是提前算好结果,然后直接保存结果。这种优化在真是生成环境中并不会发生,因为还有上下文的存在。
1:25:00
escape 函数接受一个指针作为参数,并不知道它指向数据的大小,结构,甚至是个 vector,编译器怎么能理解它的数据大小和结构呢?
编译器并不理解指针的结构。
asm volatile("" :  : "g"(p), "memory");
汇编中clobber参数用的是 "memory",意思是要 r/w 所有的内存。编译器认为,inline asm“看不见“的地方并不被认为是 memory,所以需要一个指针把它一块内存注册进入,后面 clobber() 调用时,才可以被编译器保证不被优化扔掉。
1:27:14
你有没有使用 gtest 或者其他工具来做 benchmark 的回归测试?
我个人没有做 benchmark 类的回归测试。搞编译器的人一般接受一定的性能波动,有时甚至是相当大的波动,我们需要常常进行回归测试来跟踪性能。但是一般不会使用固定的框架,因为没有固定的代表性数字标准,也没有固定的代表性机器。对于搞编译器的人,挺难使用此类方法。
1:28:08
我发现一个性能问题,我是该把它抠出来研究,还是整体一起 benchmark
都需要。先整体评估,然后把代码隔离,具体分析。
1:29:12
关于 LIKELY/UNLIKELY,有没有工具可以显示出,假如你说这是 unlikely,但是我的运行发现它和你宣称的不一致?
我的方法是人工去看。我想 VTune 或者其它工具也许有这个功能,但我还不是很清楚。
 

视频笔记 CppCon 2015: Chandler Carruth "Tuning C++: Benchmarks, and CPUs, and Compilers! Oh My!"

原文:https://www.cnblogs.com/eagledai/p/10225348.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!