C++ 语句重新排序

P. *_*one 6 c++ memory compiler-construction optimization time

这是一个关于 Chandler 在这里回答的问题(我没有足够高的代表来评论):Enforcing statement order in C++

在他的回答中,假设 foo() 没有输入或输出。它是一个黑匣子,最终可以观察到它的工作,但不会立即被需要(例如执行一些回调)。所以我们没有本地方便的输入/输出数据来告诉编译器不要优化。但我知道 foo() 会在某处修改内存,最终结果将是可观察的。在这种情况下,以下内容是否会阻止语句重新排序并获得正确的时间?

#include <chrono>
#include <iostream>

//I believe this tells the compiler that all memory everywhere will be clobbered?
//(from his cppcon talk: https://youtu.be/nXaxk27zwlk?t=2441)
__attribute__((always_inline)) inline void DoNotOptimize() {
  asm volatile("" : : : "memory");
}

// The compiler has full knowledge of the implementation.
static int ugly_global = 1; //we print this to screen sometime later
static void foo(void) { ugly_global *= 2; }

auto time_foo() {
  using Clock = std::chrono::high_resolution_clock;

  auto t1 = Clock::now();         // Statement 1
  DoNotOptimize();
  foo();                          // Statement 2
  DoNotOptimize();
  auto t2 = Clock::now();         // Statement 3

  return t2 - t1;
}
Run Code Online (Sandbox Code Playgroud)

tor*_*rek 1

\n

在这种情况下,以下内容会阻止语句重新排序并获得正确的时间吗?

\n
\n\n

这应该不是必要的,因为对的调用Clock::now应该在语言定义级别强制执行足够的排序。(也就是说,C++11 标准规定,高分辨率时钟应该以此处最有用的方式获取系统可以提供的尽可能多的信息。请参阅下面的“第二个问题”。)

\n\n

但还有一个更普遍的情况。值得思考的问题是:提供 C++ 库实现的人实际上是如何编写这个函数的? 或者,将 C++ 本身排除在外。给定一个语言标准,实现者\xe2\x80\x94(一个编写该语言\xe2\x80\x94的实现的人或团体)如何获得你所需要的东西?从根本上来说,我们需要区分语言标准的要求以及实施提供商如何实施这些要求

\n\n

语言本身可以用抽象机来表达,C 和 C++ 语言就是如此。这个抽象机器的定义相当松散:它执行某种访问数据的指令,但在很多情况下我们不知道它是如何做这些事情的,甚至不知道各种数据项有多大(固定的数据项除外) -size 整数,如int64_t) 和 os on。机器可能有也可能没有以无法寻址的方式保存事物的“寄存器”,以及可以寻址且其地址可以记录在指针中的内存:

\n\n
p = &var\n
Run Code Online (Sandbox Code Playgroud)\n\n

使值存储在p(内存或寄存器中),以便使用访问存储在(内存或寄存器\xe2\x80\x94某些机器,特别是在过去,具有可寻址寄存器)中的*p值。1var

\n\n

尽管如此,尽管有这些抽象,我们还是希望在真实的机器上运行真实的代码。真实的机器有真正的限制:一些指令可能需要特定寄存器中的特定值(想想 x86 指令集中所有奇怪的东西,或者使用特殊用途寄存器的宽结果整数乘法器和除法器,如在某些 MIPS 处理器上),或者导致 CPU 同步,或者其他什么。

\n\n

GCC 特别发明了一个约束系统来表达你可以使用机器的指令集在机器本身上做什么或不能做什么。asm随着时间的推移,它演变成具有输入、输出和破坏部分的用户可访问的结构。您展示的特定内容:

\n\n
\n
__attribute__((always_inline)) inline void DoNotOptimize() {\n  asm volatile("" : : : "memory");\n}\n
Run Code Online (Sandbox Code Playgroud)\n
\n\n

表达了这样的想法:“这条指令”(asm;实际提供的指令是空白的)“无法移动”(volatile)“并且破坏了计算机的所有内存,但没有寄存器”("memory"作为破坏部分)。

\n\n

这不是 C 或 C++ 语言的一部分。它只是一个编译器构造,受 GCC 支持,现在也受 clang 支持。但它足以强制编译器在 之前发出所有存储到内存的指令asm,并在 之后根据需要从内存中重新加载值,以防它们asm计算机执行该行中包含的(不存在的)指令时发生更改。不能保证这会在其他编译器中工作,甚至根本无法编译,但只要我们是实现者我们就选择我们要实现的编译器/与之一起实现的编译器。asm

\n\n

C++ 作为一种语言现在支持有序内存操作,这是实现者必须实现的。实现者可以使用这些asm volatile构造来实现正确的结果,前提是它们确实实现了正确的结果。例如,如果我们需要使机器本身同步\xe2\x80\x94以发出内存屏障\xe2\x80\x94 mfencemembar #sync我们可以在asm\的指令部分条款。另请参阅克劳斯在评论中提到的编译器重新排序与内存重新排序

\n\n

实现者需要找到一个适当有效的技巧,无论是否特定于编译器,以获得正确的语义,同时最大限度地减少运行时的速度下降:例如,我们可能想要使用而不是lfenceif mfencethat's不够,或者membar #LoadLoad,或者无论什么对机器来说都是正确的。如果我们的实现Clock::now需要某种奇特的内联asm,我们就编写一个。如果没有,我们就不会。我们确保生成所需的 xe2x80x94,然后系统的所有用户都可以使用它,而不需要知道我们必须调用哪种肮脏的实现技巧。

\n\n

这里还有一个第二个问题:语言规范真的按照我们认为/希望的方式限制实现者吗? 克里斯·多德的评论表明他是这么认为的,而且他在这类问题上通常是正确的。其他一些评论者则不这么认为,但我在这一点上支持克里斯·多德。我认为没有必要。不过,您始终可以编译为汇编,或反汇编已编译的程序来检查!

\n\n

如果编译器没有做正确的事情,那么asm迫使它在 GCC 和 clang 中做正确的事情。它可能无法在其他编译器中工作。

\n\n
\n\n

1特别是在 KA-10 上,寄存器只是存储器的前 16 个字。正如维基百科页面指出的那样,这意味着您可以将指令放入其中并调用它们。因为前 16 个字是寄存器,所以这些指令的运行速度比其他指令快得多。

\n