正确对你的C++代码进行性能测试——DoNotOptimize实现原理

摘要:在对某些底层特性进行性能测试的时候,通常由于编译器优化的原因导致无法准确测试某些基础能力的具体性能。的类似于Google-benchmark等一些性能测试库提供了基本的DotOptimaze之类的模板函数来避免编译器的优化,以保证准确的性能测试。本文描述了该是实现的具体细节,具体细节不是C++标准范围内,主要有编译器提供,不同编译器的实现细节不同,本文以clang为基准进行描述。
关键字:clang++,g++,asm

1 面对的问题

  当我们需要测试一些基础能力的性能时往往由于编译器优化导致无法准确的进行测试。比如下面的代码中,我们期望测试testFunc的性能,但是编译器判断testFunc对程序没有任何新的影响而删除了或者对该语句进行排序。

需要注意的是下面这种性能测试通常是可行的,所以一般不用担心这么写有任何副作用。上面提到的是一种可能性。

int main(int argc, char **argv){
    std::cout<<"Hello World\n";
    auto st = std::chrono::high_resolution_clock::now();
    testFunc();
    auto et = std::chrono::high_resolution_clock::now();
    auto str = std::format("the function run in {} ms\n", std::chrono::duration_cast<std::chrono::milliseconds>(et - st).count());
    std::cout<<str;
    return 0;
}

  编译器修改后的语句可能变成:

int main(int argc, char **argv){
    std::cout<<"Hello World\n";
    auto st = std::chrono::high_resolution_clock::now();
    auto et = std::chrono::high_resolution_clock::now();
    auto str = std::format("the function run in {} ms\n", std::chrono::duration_cast<std::chrono::milliseconds>(et - st).count());
    std::cout<<str;
    return 0;
}

  和或者

int main(int argc, char **argv){
    std::cout<<"Hello World\n";
    testFunc();
    auto st = std::chrono::high_resolution_clock::now();
    auto et = std::chrono::high_resolution_clock::now();
    auto str = std::format("the function run in {} ms\n", std::chrono::duration_cast<std::chrono::milliseconds>(et - st).count());
    std::cout<<str;
    return 0;
}

  产生这样的原因是现代处理器将都支持多级流水线执行,编译器为了性能最大化,会分析代码中的数据依赖,根据数据依赖关系和从对程序的影响来对代码进行删除、替换、排序等优化,来最大化CPU的吞吐率(比如常量传播等)。
  比如下面的代码:

int testFunc(){
    int a = 1;
    return a;
}

int main(int argc, char **argv){
    std::cout<<"Hello World\n";
    auto st = std::chrono::high_resolution_clock::now();
    testFunc();
    
    auto et = std::chrono::high_resolution_clock::now();
    auto str = std::format("the function run in {} ms\n", std::chrono::duration_cast<std::chrono::milliseconds>(et - st).count());
    std::cout<<str;
    return 0;
}

  使用clang+±18编译器O3编译的返汇编如下,从下面的代码中看不到testFunc的任何调用就是因为编译器认为这部分代码没有任何意义就删除了。

00000000000023e0 <main>:
    23e0:	41 56                	push   %r14
    23e2:	53                   	push   %rbx
    23e3:	48 83 ec 38          	sub    $0x38,%rsp
    23e7:	48 8b 1d da cb 00 00 	mov    0xcbda(%rip),%rbx        # efc8 <_ZSt4cout@GLIBCXX_3.4>
    23ee:	48 8d 35 eb 9f 00 00 	lea    0x9feb(%rip),%rsi        # c3e0 <_IO_stdin_used+0x3e0>
    23f5:	ba 0c 00 00 00       	mov    $0xc,%edx
    23fa:	48 89 df             	mov    %rbx,%rdi
    23fd:	e8 ce fd ff ff       	call   21d0 <_ZSt16__ostream_insertIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_l@plt>
    2402:	e8 29 fc ff ff       	call   2030 <_ZNSt6chrono3_V212system_clock3nowEv@plt>
    2407:	49 89 c6             	mov    %rax,%r14
    240a:	e8 21 fc ff ff       	call   2030 <_ZNSt6chrono3_V212system_clock3nowEv@plt>
    240f:	4c 29 f0             	sub    %r14,%rax
    2412:	48 b9 db 34 b6 d7 82 	movabs $0x431bde82d7b634db,%rcx
    2419:	de 1b 43 
    241c:	48 f7 e9             	imul   %rcx
    241f:	48 89 d0             	mov    %rdx,%rax
    2422:	48 c1 e8 3f          	shr    $0x3f,%rax
    2426:	48 c1 fa 12          	sar    $0x12,%rdx
    242a:	48 01 c2             	add    %rax,%rdx
    242d:	48 89 54 24 20       	mov    %rdx,0x20(%rsp)
    2432:	48 8d 15 b4 9f 00 00 	lea    0x9fb4(%rip),%rdx        # c3ed <_IO_stdin_used+0x3ed>
    2439:	48 89 e7             	mov    %rsp,%rdi
    243c:	4c 8d 44 24 20       	lea    0x20(%rsp),%r8
    2441:	be 1a 00 00 00       	mov    $0x1a,%esi
    2446:	b9 51 00 00 00       	mov    $0x51,%ecx
    244b:	e8 50 00 00 00       	call   24a0 <_ZSt7vformatB5cxx11St17basic_string_viewIcSt11char_traitsIcEESt17basic_format_argsISt20basic_format_contextINSt8__format10_Sink_iterIcEEcEE>
    2450:	48 8b 34 24          	mov    (%rsp),%rsi
    2454:	48 8b 54 24 08       	mov    0x8(%rsp),%rdx
    2459:	48 89 df             	mov    %rbx,%rdi
    245c:	e8 6f fd ff ff       	call   21d0 <_ZSt16__ostream_insertIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_l@plt>
    2461:	48 8b 3c 24          	mov    (%rsp),%rdi
    2465:	48 8d 44 24 10       	lea    0x10(%rsp),%rax
    246a:	48 39 c7             	cmp    %rax,%rdi
    246d:	74 05                	je     2474 <main+0x94>
    246f:	e8 fc fc ff ff       	call   2170 <_ZdlPv@plt>
    2474:	31 c0                	xor    %eax,%eax
    2476:	48 83 c4 38          	add    $0x38,%rsp
    247a:	5b                   	pop    %rbx
    247b:	41 5e                	pop    %r14
    247d:	c3                   	ret
    247e:	48 89 c3             	mov    %rax,%rbx
    2481:	48 8b 3c 24          	mov    (%rsp),%rdi
    2485:	48 8d 44 24 10       	lea    0x10(%rsp),%rax
    248a:	48 39 c7             	cmp    %rax,%rdi
    248d:	74 05                	je     2494 <main+0xb4>
    248f:	e8 dc fc ff ff       	call   2170 <_ZdlPv@plt>
    2494:	48 89 df             	mov    %rbx,%rdi
    2497:	e8 e4 fd ff ff       	call   2280 <_Unwind_Resume@plt>
    249c:	0f 1f 40 00          	nopl   0x0(%rax)

  可能有些时候我们就是为了测试一些和硬件相关的场景的性能,比如lock-free队列的性能之类,如何避免编译器优化来保证性能测试结果正常?首先肯定不能通过编译器选项来实现,我们只是为了减少编译器优化对测试时间计算的影响而不是希望编译器不做任何优化。Google-Benchmark中提供了一种实现思路,即使用内联汇编组织编译指令重排详情见Google-Benchmark-DoNotOptimize

2 DoNotOptimize

2.1 简介

  DoNotOptimize是google-benchmark提供的一个函数,强制编译器不要对制定的变量进行任何优化。需要注意的是DoNotOptimize强制编译器不要对制定的不指定的编译器月变量优化而不是不对表达式进行优化,也就是说你的表达式可以被优化的化依然会被优化只是结果一定写内存。

The DoNotOptimize(…) function can be used to prevent a value or expression from being optimized away by the compiler. This function is intended to add little to no overhead.

DoNotOptimize() works by forcing the result of to be stored to memory, which in turn forces the compiler to actually evaluate . It does not prevent the compiler from optimizing the evaluation of but it does prevent the expression from being discarded completely.

  比如下面的例子:

int testFunc(int a){
    return a;
}

int main(int argc, char **argv){
    int a = 1;
    DoNotOptimize(a);
    auto t = testFunc(a);
    DoNotOptimize(t);
    return 0;
}

  不加DoNotOptimize的化testFunc、a和t都会被删除,而编译后的结果如下。可以看到两个变量都是读取的栈内存。

0000000000001140 <main>:
    1140:	c7 44 24 f8 01 00 00 	movl   $0x1,-0x8(%rsp)
    1147:	00 
    1148:	8b 44 24 f8          	mov    -0x8(%rsp),%eax
    114c:	89 44 24 fc          	mov    %eax,-0x4(%rsp)
    1150:	31 c0                	xor    %eax,%eax
    1152:	c3                   	ret

2.2 实现原理

  具体实现是使用编译器的asm扩展实现,不同编译器实现原理不同,本文主要聚焦clang/gcc实现,msvc有兴趣的同学自己看。

template <class Tp>
inline BENCHMARK_ALWAYS_INLINE void DoNotOptimize(Tp& value) {
#if defined(__clang__)
  asm volatile("" : "+r,m"(value) : : "memory");
#else
  asm volatile("" : "+m,r"(value) : : "memory");
#endif
}

  由于clang和gcc的实现类似,只存在细节上的差异,这里只描述gcc的。

  • https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html
  • https://releases.llvm.org/14.0.0/tools/clang/docs/LanguageExtensions.html

  DoNotOptimize的实现使用了编译内链汇编的扩展,具体语法如下:

asm asm-qualifiers ( AssemblerTemplate 
                 : OutputOperands 
                 [ : InputOperands
                 [ : Clobbers ] ])
  • volatile:asm-qualifiers标记为volatile表示禁止编译器优化;
  • "":一个空语句;
  • "+r,m"(value):约束value读写内存的行为,+表示这是一个读/写操作数,r表示可以使用通用寄存器,m表示可以使用内存;
  • memory:是一个内存屏障,告诉编译器以此语句为分界线,上面的语句不能排序到下面,下面的语句不能排序到上面,来保证执行顺序。

  volatile的唯一作用就是告诉编译器这个变量不能被优化必须经过寄存器读写到内存,而不是直接操作内存。而"+r,m"(value)限制了具体读写的行为。memory为了保证执行语义而存在,比如程序:

int testFunc(int a){
    return a;
}

int main(int argc, char **argv){
    int a = 1;
    auto t = clock();
    DoNotOptimize(a);
    auto c = testFunc(a);
    DoNotOptimize(t);
    auto t2 = clock();
    return 0;
}

  如果没有memory的存在,at便来仍然会读写内存不会被优化,但是不同语句的执行顺序不能严格保证的。auto c=testFunc(a)auto t=clock()由于两个语句质量没有任何的关联性,在编译器看来对该语句进行排序不会有任何副作用,但是实际上的你的意图是计算耗时排序反而会导致计算不准确。

3 总结

  为了精确的测试程序的耗时,尽量在测试区间添加DonotOptimize,避免编译器优化而导致测试错误。

4 参考文献

  • https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html
  • https://github.com/google/benchmark/blob/0baacde3618ca617da95375e0af13ce1baadea47/include/benchmark/benchmark.h#L331-L337
  • https://github.com/google/benchmark/blob/e451e50e9b8af453f076dec10bd6890847f1624e/include/benchmark/benchmark.h#L339-L368
  • https://releases.llvm.org/14.0.0/tools/clang/docs/LanguageExtensions.html
  • https://github.com/google/benchmark/issues/242
  • https://theunixzoo.co.uk/blog/2021-10-14-preventing-optimisations.html
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值