视频笔记 CppCon 2015: Chandler Carruth "Tuning C++: Benchmarks, and CPUs, and Compilers! Oh My!"
视频笔记 CppCon 2015: Chandler Carruth “Tuning C++: Benchmarks, and CPUs, and Compilers! Oh My!”
CppCon 2015: Chandler Carruth “Tuning C++: Benchmarks, and CPUs, and Compilers! Oh My!”
- 转载请说明出处 http://www.cnblogs.com/eagledai/
-
视频中“废话”有点多,另外视频略微有点老,使用的编译器和OS都不一样,程序运行命令和结果都和笔记中的有部分不同。
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
#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
#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
视频截图却是:
perf record -g ./bench perf report -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,全部被编译器优化掉了!编译器发现代码什么都没有做,干脆删掉。
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"); }
-
告诉编译器我们要往任何内存写数据,或者从任何内存读数据,但其实什么也没有做,没有代码产生
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);
重新编译,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,并不奇怪
#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();
------------------------------------------------------------------ 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
-
出乎预料,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
-
慢的原因是,很多跳转,造成 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 的快了一些,
-
优化过的机器码认为跳转发生的概率低,基本可以不跳转,一路向下走
-
下面(未显示部分)的 idiv 部分,尽管只有20%的概率命中,仍然占据了CPU计算的主要时间
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);
------------------------------------------------------------------
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
视频 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 或者其它工具也许有这个功能,但我还不是很清楚。
|