在 C 中强制执行语句顺序

新手上路,请多包涵

假设我有许多要按固定顺序执行的语句。我想使用优化级别 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;

原文由 S2108887 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 875
2 个回答

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

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

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

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

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

基本的认识是,算术是不可观察的(即使在时序级别)这一事实渗透到计算机的各个层。对于编译器、运行时甚至硬件来说都是如此。强制它是可观察的既会极大地限制编译器,也会极大地限制硬件。

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

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

这些技术的关键是关注数据。您使计算的输入对优化器不透明,而计算的结果对优化器不透明。一旦你这样做了,你就可以可靠地计时。让我们看一下原始问题中示例的实际版本,但 foo 的定义对实现完全可见。我还从您可以在此处找到的 Google Benchmark 库中提取了 DoNotOptimize 的(非便携式)版本: https ://github.com/google/benchmark/blob/v1.0.0/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 的可能性。

原文由 Chandler Carruth 发布,翻译遵循 CC BY-SA 4.0 许可协议

概括:

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

原答案:

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

原文由 Jeremy 发布,翻译遵循 CC BY-SA 3.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题