在C ++中执行语句顺序

S2108887:

假设我有许多要按固定顺序执行的语句。我想在优化级别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;
钱德勒·卡鲁斯(Chandler Carruth):

与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对于实现完全可见。我还DoNotOptimize从Google Benchmark库中提取了一个(非便携式)版本,您可以在这里找到它: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的可能性

本文收集自互联网,转载请注明来源。

如有侵权,请联系 [email protected] 删除。

编辑于
0

我来说两句

0 条评论
登录 后参与评论

相关文章