在C ++中执行语句顺序

假设我有一些要按固定顺序执行的语句。 我想用优化级别为2的g ++,所以有些语句可以重新sorting。 有什么工具可以执行一定的语句顺序?

考虑下面的例子。

using Clock = std::chrono::high_resolution_clock; auto t1 = Clock::now(); // Statement 1 foo(); // Statement 2 auto t2 = Clock::now(); // Statement 3 auto elapsedTime = t2 - t1; 

在这个例子中,语句1-3以给定的顺序执行是很重要的。 但是,编译器不能认为语句2是独立于1和3,并执行代码如下所示?

 using Clock=std::chrono::high_resolution_clock; foo(); // Statement 2 auto t1 = Clock::now(); // Statement 1 auto t2 = Clock::now(); // Statement 3 auto elapsedTime = t2 - t1; 

在与C ++标准委员会讨论之后,我想尝试提供一个更全面的答案。 除了成为C ++委员会成员之外,我也是LLVM和Clang编译器的开发人员。

从根本上说,没有办法在序列中使用屏障或某些操作来实现这些转换。 基本的问题是,像整数加法这样的操作语义完全是已知的实现。 它可以模拟它们,它知道它们不能被正确的程序观察到,并且总是可以随意移动它们。

我们可以设法防止这种情况发生,但是会有非常负面的结果,最终会失败。

首先,在编译器中防止这种情况的唯一方法是告诉它,所有这些基本操作都是可观察的。 问题是这样会阻止绝大多数的编译器优化。 在编译器内部,我们基本上没有很好的机制来build模时间是可观察的,但没有别的。 我们甚至没有一个好的模型来说明什么是需要时间的 。 例如,将32位无符号整数转换为64位无符号整数是否需要时间? 在x86-64上花费的时间是零,但是在其他的体系结构上花费的时间是非零的。 这里没有一般正确的答案。

但即使我们通过一些英雄的成功来阻止编译器对这些操作进行重新sorting,但是这并不足以保证。 考虑在x86机器上执行C ++程序的一种有效且符合要求的方法:DynamoRIO。 这是一个dynamic评估程序机器码的系统。 它可以做的一件事就是在线优化,它甚至能够在计时之外推测性地执行整个范围的基本算术指令。 而且这种行为对于dynamic评估者来说并不是唯一的,实际的x86 CPU也会推测(数量less得多)的指令并dynamic地重新排列它们。

基本的实现是,算术不可观测(甚至在时间级别)的事实是渗透到计算机层面的东西。 编译器,运行时,甚至硬件都是如此。 强制它是可观察的将大大限制编译器,但它也将大大限制硬件。

但是这一切都不应该让你失去希望。 当你想要执行基本的math运算的时候,我们已经学习了可靠工作的技巧。 通常这些在进行微基准testing时使用 。 我在CppCon2015上谈了这个: https ://youtu.be/nXaxk27zwlk

那里显示的技术也提供了各种微型基准库,如谷歌的: https : //github.com/google/benchmark#preventing-optimisation

这些技术的关键是把重点放在数据上。 您将优化程序的inputinput到不透明的计算中,并将优化程序的计算结果不透明。 一旦你完成了,你可以可靠地计时。 让我们看一下原始问题中一个现实版本的例子,但是foo的定义对于实现是完全可见的。 我还从Google Benchmark库中提取了(不可移植的) DoNotOptimize版本,您可以在这里find: https : //github.com/google/benchmark/blob/master/include/benchmark/benchmark_api.h#L208

 #include <chrono> template <class T> __attribute__((always_inline)) inline void DoNotOptimize(const T &value) { asm volatile("" : "+m"(const_cast<T &>(value))); } // The compiler has full knowledge of the implementation. static int foo(int x) { return x * 2; } auto time_foo() { using Clock = std::chrono::high_resolution_clock; auto input = 42; auto t1 = Clock::now(); // Statement 1 DoNotOptimize(input); auto output = foo(input); // Statement 2 DoNotOptimize(output); auto t2 = Clock::now(); // Statement 3 return t2 - t1; } 

在这里,我们确保input数据和输出数据在计算foo周围被标记为不可优化的,并且只在这些标记周围计算时间。 因为你正在使用数据来钳制计算,所以保证在两个时间之间,而且计算本身可以被优化。 最近由Clang / LLVM构build的由此产生的x86-64程序集是:

 % ./bin/clang++ -std=c++14 -c -S -o - so.cpp -O3 .text .file "so.cpp" .globl _Z8time_foov .p2align 4, 0x90 .type _Z8time_foov,@function _Z8time_foov: # @_Z8time_foov .cfi_startproc # BB#0: # %entry pushq %rbx .Ltmp0: .cfi_def_cfa_offset 16 subq $16, %rsp .Ltmp1: .cfi_def_cfa_offset 32 .Ltmp2: .cfi_offset %rbx, -16 movl $42, 8(%rsp) callq _ZNSt6chrono3_V212system_clock3nowEv movq %rax, %rbx #APP #NO_APP movl 8(%rsp), %eax addl %eax, %eax # This is "foo"! movl %eax, 12(%rsp) #APP #NO_APP callq _ZNSt6chrono3_V212system_clock3nowEv subq %rbx, %rax addq $16, %rsp popq %rbx retq .Lfunc_end0: .size _Z8time_foov, .Lfunc_end0-_Z8time_foov .cfi_endproc .ident "clang version 3.9.0 (trunk 273389) (llvm/trunk 273380)" .section ".note.GNU-stack","",@progbits 

在这里,你可以看到编译器优化了对foo(input)的调用,使其只有一条指令addl %eax, %eax ,但是不用将其移动到定时之外,或者完全消除它,即使input不变。

希望这会有所帮助,C ++标准委员会正在考虑在这里标准化类似于DoNotOptimize API的可能性。

概要:

似乎没有保证重新sorting的方法,但只要没有启用链接时间/完整程序优化, 将被调用函数定位在单独的编译单元中似乎是一个相当不错的select 。 (至less对于GCC来说,虽然逻辑上可能会暗示其他编译器也可能会这样做)。这是以函数调用为代价的。内联代码根据定义在同一个编译单元中,并且可以重新sorting。

原始答案:

GCC在-O2优化下重新调用呼叫:

 #include <chrono> static int foo(int x) // 'static' or not here doesn't affect ordering. { return x*2; } int fred(int x) { auto t1 = std::chrono::high_resolution_clock::now(); int y = foo(x); auto t2 = std::chrono::high_resolution_clock::now(); return y; } 

GCC 5.3.0:

g++ -S --std=c++11 -O0 fred.cpp

 _ZL3fooi: pushq %rbp movq %rsp, %rbp movl %ecx, 16(%rbp) movl 16(%rbp), %eax addl %eax, %eax popq %rbp ret _Z4fredi: pushq %rbp movq %rsp, %rbp subq $64, %rsp movl %ecx, 16(%rbp) call _ZNSt6chrono3_V212system_clock3nowEv movq %rax, -16(%rbp) movl 16(%rbp), %ecx call _ZL3fooi movl %eax, -4(%rbp) call _ZNSt6chrono3_V212system_clock3nowEv movq %rax, -32(%rbp) movl -4(%rbp), %eax addq $64, %rsp popq %rbp ret 

但:

g++ -S --std=c++11 -O2 fred.cpp

 _Z4fredi: pushq %rbx subq $32, %rsp movl %ecx, %ebx call _ZNSt6chrono3_V212system_clock3nowEv call _ZNSt6chrono3_V212system_clock3nowEv leal (%rbx,%rbx), %eax addq $32, %rsp popq %rbx ret 

现在,用foo()作为外部函数:

 #include <chrono> int foo(int x); int fred(int x) { auto t1 = std::chrono::high_resolution_clock::now(); int y = foo(x); auto t2 = std::chrono::high_resolution_clock::now(); return y; } 

g++ -S --std=c++11 -O2 fred.cpp

 _Z4fredi: pushq %rbx subq $32, %rsp movl %ecx, %ebx call _ZNSt6chrono3_V212system_clock3nowEv movl %ebx, %ecx call _Z3fooi movl %eax, %ebx call _ZNSt6chrono3_V212system_clock3nowEv movl %ebx, %eax addq $32, %rsp popq %rbx ret 

但是,如果这是链接-flto(链接时优化):

 0000000100401710 <main>: 100401710: 53 push %rbx 100401711: 48 83 ec 20 sub $0x20,%rsp 100401715: 89 cb mov %ecx,%ebx 100401717: e8 e4 ff ff ff callq 100401700 <__main> 10040171c: e8 bf f9 ff ff callq 1004010e0 <_ZNSt6chrono3_V212system_clock3nowEv> 100401721: e8 ba f9 ff ff callq 1004010e0 <_ZNSt6chrono3_V212system_clock3nowEv> 100401726: 8d 04 1b lea (%rbx,%rbx,1),%eax 100401729: 48 83 c4 20 add $0x20,%rsp 10040172d: 5b pop %rbx 10040172e: c3 retq 

重新sorting可能由编译器或处理器完成。

大多数编译器提供了一个平台特定的方法来防止读写指令的重新sorting。 在海湾合作委员会,这是

 asm volatile("" ::: "memory"); 

( 更多信息在这里 )

请注意,这只是间接地阻止重新sorting操作,只要它们依赖于读取/写入。

在实践中,我还没有看到系统调用Clock::now()确实具有与此类障碍相同的效果。 你可以检查生成的程序集确定。

然而,在编译期间被测函数得到评估的情况并不less见。 为了执行“现实”执行,您可能需要从I / O或volatile读取foo()input。


另一个select是禁用foo()内联 – 再次,这是编译器特定的,通常不是可移植的,但会有相同的效果。

在gcc上,这将是__attribute__ ((noinline))


@Ruslan提出了一个基本问题:这种测量有多现实?

执行时间受很多因素影响:一个是我们正在运行的实际硬件,另一个是并发访问共享资源,如caching,内存,磁盘和CPU内核。

那么我们通常做些什么来获得可比的时间:确保它们具有低的误差余量的可重现性 。 这使他们有点人为的。

“高速caching”与“低温高速caching”的执行性能可能会轻易相差一个数量级 – 但实际上,它会介于两者之间(“温热”)?

C ++语言以多种方式定义了可观察的内容。

如果foo()不做任何可观察的事情,那么它可以完全消除。 如果foo()只执行一个将值存储在“local”状态(在堆栈或某个对象中)的计算,编译器可以certificate没有安全派生的指针可以进入Clock::now()代码,那么移动Clock::now()调用没有明显的后果。

如果foo()与文件或显示交互,而且编译器不能certificateClock::now()不与文件或显示交互,则重新sorting无法完成,因为与文件或显示的交互是可观察的行为。

虽然您可以使用特定于编译器的黑客手段强制代码不移动(如内联汇编),但另一种方法是尝试智能化编译器。

创build一个dynamic加载的库。 在代码之前加载它。

这个库暴露了一件事情:

 namespace details { void execute( void(*)(void*), void *); } 

并像这样包装它:

 template<class F> void execute( F f ) { struct bundle_t { F f; } bundle = {std::forward<F>(f)}; auto tmp_f = [](void* ptr)->void { auto* pb = static_cast<bundle_t*>(ptr); (pb->f)(); }; details::execute( tmp_f, &bundle ); } 

它包装一个无限的lambda,并使用dynamic库在编译器无法理解的上下文中运行它。

在dynamic库里面,我们做:

 void details::execute( void(*f)(void*), void *p) { f(p); } 

这很简单。

现在重新sorting要execute的调用,它必须了解dynamic库,在编译你的testing代码时它不能。

它仍然可以消除foo()的零副作用,但是你赢了一些,你输了一些。

不,它不能。 根据C ++标准[intro.execution]:

每一个与全expression式相关的数值计算和副作用在每一个与下一个要被评估的完整expression式相关的数值计算和副作用之前被sorting。

一个完整的expression式基本上是一个以分号结尾的语句。 正如你所看到的,上面的规则规定了语句必须按顺序执行。 声明中,允许编译器更加自由地控制(即,在某些情况下,允许评估构成除了从左到右或其他任何特定顺序之外的语句的expression式)。

请注意,在这里没有满足应用规则的条件。 认为任何编译器都能够certificate重新调用调用系统时间不会影响可观察的程序行为是不合理的。 如果出现这样一种情况,即可以在不改变观察到的行为的情况下对两次调用进行重新sorting,那么实际上产生一个编译器来分析一个具有足够理解的程序是非常低效的,以便能够确定地推断出这一点。

没有。

有时候,通过“假设”规则,陈述可能被重新sorting。 这不是因为它们在逻辑上彼此独立,而是因为独立性允许这样的重新sorting在不改变程序的语义的情况下发生。

移动获取当前时间的系统调用显然不满足该条件。 一个知情或不知情的编译器是不符合规定的,而且很愚蠢。

一般来说,我不希望任何导致系统调用的expression式即使是一个积极优化的编译器也是“二次猜测”的。 它只是不够了解那个系统调用。

Interesting Posts