在C ++中强制执行语句顺序

时间:2016-06-13 09:47:33

标签: c++ c++11 operator-precedence

假设我有许多我想要执行的语句 固定的订单。我想使用优化级别为2的g ++,所以有些 声明可以重新排序。人们需要使用哪些工具来强制执行某些语句排序?

请考虑以下示例。

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;

7 个答案:

答案 0 :(得分:83)

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

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

我们可以尝试阻止这种情况,但它会产生非常负面的结果并最终会失败。

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

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

基本的实现是,算术不可观察(甚至在时间级别)这一事实渗透到计算机的各个层面。对于编译器,运行时,甚至硬件来说都是如此。强制它可观察会严重限制编译器,但它也会大大限制硬件。

但所有这一切都不应该让你失去希望。当您想要执行基本的数学运算时,我们已经研究了可靠的技术。通常,在进行微基准测试时会使用这些。我在CppCon2015上讨论了这个问题:https://youtu.be/nXaxk27zwlk

此处显示的技术也由各种微型基准库提供,例如Google:https://github.com/google/benchmark#preventing-optimisation

这些技术的关键是关注数据。您使计算的输入对优化程序不透明,并且计算结果对优化程序不透明。一旦你完成了,你可以可靠地计时。让我们看看原始问题中示例的实际版本,但foo的定义对于实现完全可见。我还从Google基准库中提取了DoNotOptimize的(非便携式)版本,您可以在此处找到它: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;
}

这里我们确保输入数据和输出数据在计算foo周围被标记为不可优化,并且只有这些标记周围才是计算的时间。因为您正在使用数据来计算计算,所以它保证保持在两个时间之间,但允许计算本身进行优化。最近构建的Clang / LLVM生成的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,但不会将其移出时间之外,或者尽管输入不断而完全消除它。

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

答案 1 :(得分:60)

<强>要点:

似乎没有保证防止重新排序的方法,但只要未启用链接时/全程序优化,在单独的编译单元中定位被调用的函数似乎是一个相当不错的选择。 (至少在GCC中,虽然逻辑表明这也可能与其他编译器有关。)这是以函数调用为代价的 - 内联代码根据定义在同一个编译单元中并且可以重新排序。

原始回答:

GCC重新排序-O2优化下的调用:

S.N.,Traded Companies,No. Of Transaction,Max Price,Min Price,Closing Price,Traded Shares,Amount,Previous Closing,Difference Rs.
1,Yeti  Development Bank Limited,7,143.00,140.00,140.00,1777.00,252897.00,143.00,-3.00
2,Western Development Bank Limited,1,315.00,315.00,315.00,500.00,157500.00,319.00,-4.00
3,Vijaya laghubitta Bittiya Sanstha Ltd.,162,1235.00,1172.00,1195.00,3188.00,3853176.00,1225.00,-30.00
4,United Insurance Co. (Nepal) Ltd.,111,550.00,500.00,540.00,50433.00,26399127.00,510.00,30.00
5,Unique Finance Ltd.,6,147.00,145.00,147.00,280.00,41116.00,143.00,4.00
6,Uniliver Nepal Limited,1,25500.00,25500.00,25500.00,80.00,2040000.00,25186.00,314.00
7,Tourism Development Bank Limited,2,233.00,229.00,229.00,51.00,11719.00,229.00,0.00
8,Tinau Development Bank Limited,1,300.00,300.00,300.00,154.00,46200.00,300.00,0.00
9,Taragaon Regency Hotel Limited,4,221.00,213.00,216.00,1550.00,334550.00,217.00,-1.00
10,Synergy Finance Ltd.,6,100.00,97.00,100.00,7991.00,793118.00,98.00,2.00
11,Swarojgar Laghu Bitta Bikas Bank Ltd.,9,2040.00,1921.00,2025.00,932.00,1859391.00,2000.00,25.00
12,Swabalamban Bikas Bank Limited,36,2025.00,1940.00,1940.00,3024.00,5970152.00,1949.00,-9.00
13,Surya Life Insurance Company Limited,27,826.00,773.00,790.00,5354.00,4322799.00,804.00,-14.00
14,Sunrise Bank Limited,7,415.00,408.00,415.00,2162.00,891160.00,412.00,3.00
......................................

GCC 5.3.0:

#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; }

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

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

_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

#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

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

_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

答案 2 :(得分:19)

重新排序可以由编译器或处理器完成。

大多数编译器都提供特定于平台的方法来防止读写指令的重新排序。在gcc上,这是

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

More information here

请注意,这只会间接阻止重新排序操作,只要它们依赖于读/写。

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

然而,在编译期间对被测函数进行评估并不罕见。执行&#34;现实&#34;执行时,您可能需要从I / O或foo()读取派生volatile的输入。

另一种选择是禁用foo()的内联 - 再次,这是编译器特定的,通常不可移植,但会产生相同的效果。

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

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

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

所以我们通常做的是获得可比较的时序:确保它们可重现且误差率较低。这使他们有点人为。

&#34;热门缓存&#34; vs.&#34;冷缓存&#34;执行表现很容易在一个数量级上有所不同 - 但实际上,它会介于两者之间(&#34;不冷不热?#34;?)

答案 3 :(得分:10)

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

如果foo()没有任何可观察的内容,那么它可以完全消除。如果foo()仅执行将值存储在&#34; local&#34;状态(无论是在堆栈上还是在某个对象中),编译器可以证明没有安全派生的指针可以进入Clock::now()代码,那么就没有可观察到的后果移动Clock::now()来电。

如果foo()与文件或显示进行交互,并且编译器无法证明Clock::now() 与文件或显示进行交互,则无法进行重新排序,因为与文件或显示的交互是可观察的行为。

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

创建动态加载的库。在相关代码之前加载它。

该库暴露了一件事:

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 );
}

汇总了一个nullary lambda并使用动态库在编译器无法理解的上下文中运行它。

在动态库中,我们执行:

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

非常简单。

现在要重新排序对execute的调用,它必须理解动态库,它在编译测试代码时无法理解。

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

答案 4 :(得分:3)

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

  

14与a相关的每个值计算和副作用   full-expression在每个值计算和side之前排序   与要评估的下一个完整表达相关联的效果。

完整表达式基本上是以分号结束的语句。正如您所看到的,上述规则规定必须按顺序执行语句。 语句中允许编译器更加自由(即,在某些情况下允许评估构成语句的表达式,而不是从左到右或其他任何特定的顺序)。

请注意,此处不符合要应用的as-if规则的条件。认为任何编译器能够证明重新排序调用以获得系统时间不会影响可观察的程序行为是不合理的。如果有一种情况可以在不改变观察到的行为的情况下重新排序两次获取时间的调用,那么实际生成一个编译器来分析程序的能力是非常低效的,该编译器具有足够的理解能够确定地推断出这一点。

答案 5 :(得分:2)

有时,通过&#34; as-if&#34;规则,声明可能会被重新订购。这不是因为它们在逻辑上彼此独立,而是因为这种独立性允许在不改变程序语义的情况下进行这种重新排序。

移动获取当前时间的系统调用显然不满足该条件。知情或不知情的编译器是不合规的并且非常愚蠢。

一般情况下,我不会指望任何导致系统调用的表达式是&#34;第二次猜测&#34;通过积极优化编译器。它对系统调用的作用还不够了解。

答案 6 :(得分:0)

noinline函数+内联汇编黑盒+完整的数据依赖项

这是基于https://stackoverflow.com/a/38025837/895245的,但是由于我看不到为何无法在::now()上重新排序的明确理由,因此,我偏执地将其与noinline函数一起放在asm。

通过这种方式,我非常确定不会发生重新排序,因为noinline::now和数据依赖项“绑定”。

main.cpp

#include <chrono>
#include <iostream>
#include <string>

// noinline ensures that the ::now() cannot be split from the __asm__
template <class T>
__attribute__((noinline)) auto get_clock(T& value) {
    // Make the compiler think we actually use / modify the value.
    // It can't "see" what is going on inside the assembly string.
    __asm__ __volatile__("" : "+m"(value));
    return std::chrono::high_resolution_clock::now();
}

template <class T>
static T foo(T niters) {
    T result = 42;
    for (T i = 0; i < niters; ++i) {
        result = (result * result) - (3 * result) + 1;
    }
    return result;
}

int main(int argc, char **argv) {
    unsigned long long input;
    if (argc > 1) {
        input = std::stoull(argv[1], NULL, 0);
    } else {
        input = 10000;
    }

    // Must come before because it could modify input
    // which is passed as a reference.
    auto t1 = get_clock(input);
    auto output = foo(input);
    // Must come after as it could use the output.
    auto t2 = get_clock(output);
    std::cout << "output " << output << std::endl;
    std::cout << "time "
              << std::chrono::duration_cast<std::chrono::nanoseconds>(t2 - t1).count()
              << std::endl;
}

编译并运行:

g++ -ggdb3 -O3 -std=c++14 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out 1000
./main.out 10000
./main.out 100000

该方法的唯一次要缺点是,我们在callq方法上添加了一条额外的inline指令。 objdump -CD显示main包含:

    11b5:       e8 26 03 00 00          callq  14e0 <auto get_clock<unsigned long long>(unsigned long long&)>
    11ba:       48 8b 34 24             mov    (%rsp),%rsi
    11be:       48 89 c5                mov    %rax,%rbp
    11c1:       b8 2a 00 00 00          mov    $0x2a,%eax
    11c6:       48 85 f6                test   %rsi,%rsi
    11c9:       74 1a                   je     11e5 <main+0x65>
    11cb:       31 d2                   xor    %edx,%edx
    11cd:       0f 1f 00                nopl   (%rax)
    11d0:       48 8d 48 fd             lea    -0x3(%rax),%rcx
    11d4:       48 83 c2 01             add    $0x1,%rdx
    11d8:       48 0f af c1             imul   %rcx,%rax
    11dc:       48 83 c0 01             add    $0x1,%rax
    11e0:       48 39 d6                cmp    %rdx,%rsi
    11e3:       75 eb                   jne    11d0 <main+0x50>
    11e5:       48 89 df                mov    %rbx,%rdi
    11e8:       48 89 44 24 08          mov    %rax,0x8(%rsp)
    11ed:       e8 ee 02 00 00          callq  14e0 <auto get_clock<unsigned long long>(unsigned long long&)>

所以我们看到foo是内联的,但是get_clock不是内联的。

get_clock本身非常高效,它由一个甚至没有触及堆栈的单叶调用优化指令组成:

00000000000014e0 <auto get_clock<unsigned long long>(unsigned long long&)>:
    14e0:       e9 5b fb ff ff          jmpq   1040 <std::chrono::_V2::system_clock::now()@plt>

由于时钟精度本身受到限制,我认为您不太可能注意到额外的jmpq的时序影响。请注意,由于call位于共享库中,因此需要一个::now()

从内联汇编中调用具有数据依赖性的::now()

这可能是最有效的解决方案,甚至可以克服上面提到的jmpq

很遗憾,这很难正确完成,如Calling printf in extended inline ASM

所示。

但是,如果您的时间测量可以直接在内联汇编中完成而无需致电,则可以使用此技术。例如gem5 magic instrumentation instructions,x86 RDTSC(不确定是否能代表)和其他性能计数器就是这种情况。

在GCC 8.3.0,Ubuntu 19.04上进行了测试。