c++ - 可以在 C++ 内存模型中合并原子负载吗?

标签 c++ language-lawyer compiler-optimization memory-model stdatomic

考虑下面的 C++ 11 代码段。对于 GCC 和 clang,这编译为两个(顺序一致的)foo 负载。 (编者注:编译器不优化原子,请参阅 this Q&A 了解更多详细信息,尤其是 http://wg21.link/n4455 标准讨论关于这可能产生的问题,该标准没有为程序员提供解决工具。这个语言律师问答是关于当前标准,而不是编译器所做的。)

C++ 内存模型是否允许编译器将这两个加载合并为一个加载并为 x 和 y 使用相同的值?

(编者注:这是标准组正在研究的内容:http://wg21.link/n4455http://wg21.link/p0062。当前纸上标准允许不受欢迎的行为。)


我认为它无法合并这些负载,因为这意味着轮询原子不再起作用,但我在内存模型文档中找不到相关部分。

#include <atomic>
#include <cstdio>

std::atomic<int> foo;

int main(int argc, char **argv)
{
    int x = foo;
    int y = foo;

    printf("%d %d\n", x, y);
    return 0;
}

最佳答案

是的,因为我们无法观察到差异!

允许实现将您的代码段转换为以下代码(伪实现)。

int __loaded_foo = foo;

int x = __loaded_foo;
int y = __loaded_foo;

原因是您无法观察到上述差异,并且在保证顺序一致性的情况下,两个单独的 foo 加载。

Note: It is not just the compiler that can make such an optimization, the processor can simply reason that there is no way in which you can observe the difference and load the value of foo once — even though the compiler might have asked it to do it twice.





说明

给定一个以增量方式不断更新 foo 的线程,您可以保证 yx 的内容相比,将具有相同的稍后写入的值.

// thread 1 - The Writer
while (true) {
  foo += 1;
}
// thread 2 - The Reader
while (true) {
  int x = foo;
  int y = foo;

  assert (y >= x); // will never fire, unless UB (foo has reached max value)
}                  

想象编写线程由于某种原因在每次迭代时暂停其执行(因为 context-switch 或其他实现定义的原因);您无法证明这是导致这两个 x 的原因。和 y具有相同的值,或者是因为“合并优化”。


换句话说,我们必须给定本节中的代码的潜在结果:

  1. 在两次读取 (x == y) 之间没有向 foo 写入新值。
  2. 在两次读取 (x < y) 之间将新值写入 foo

由于这两种情况中的任何一种都可能发生,因此实现可以自由地缩小范围以始终简单地执行其中一种;我们无法观察到差异。





标准是怎么说的?

只要我们无法观察到我们表达的行为与执行期间的行为之间的任何差异,实现就可以进行任何它想要的更改。

这在 [intro.execution]p1 中有介绍:

The semantic descriptions in this International Standard define a parameterized nondeterministic abstract machine. This International Standard places no requirement on the structure of conforming implementations. In particular, they need not copy or emulate the structure of the abstract machine. Rather, conforming implementations are required to emulate (only) the observable behavior of the abstract machine as explained below.

另一个更清楚的部分 [intro.execution]p5 :

A conforming implementation executing a well-formed program shall produce the same observable behavior as one of the possible executions of the corresponding instance of the abstract machine with the same program and the same input.

进一步阅读:





循环轮询呢?

// initial state
std::atomic<int> foo = 0;
// thread 1
while (true) {
  if (foo)
    break;
}
// thread 2
foo = 1

Question: Given the reasoning in the previous sections, could an implementation simply read foo once in thread 1, and then never break out of the loop even if thread 2 writes to foo?

答案;没有。

在顺序一致的环境中,我们保证在 thread 2 中写入 foo 将在 thread 1 中可见;这意味着当写入发生时,线程 1 必须观察这种状态变化。

注意:一个实现可以将两个读取转换为一个,因为我们无法观察到差异(一个 fence 与两个一样有效),但它不能完全忽略一个读取它本身存在。

注意:本节内容由[atomics.order]p3-4保证.





如果我真的想阻止这种形式的“优化”怎么办?

如果您想强制实现在您编写它的每个点实际读取某个变量的值,您应该查看 volatile 的用法(请注意,这绝不会增强线程安全性)。

但实际上编译器不会优化原子,标准组已建议不要使用 volatile atomic出于这种原因,直到尘埃落定在这个问题上。见

关于c++ - 可以在 C++ 内存模型中合并原子负载吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/33127973/

相关文章:

c++ - 是否可以使用强制编译器错误扩展 typedef?

C++ 本身的结构?

c++ - 初始化派生结构的基础部分/意外打包派生结构字段到基础结构的对齐间隙

c# - 为什么只读字段检查不能在循环之外进行优化?

c++ - 是否有理由不使用链接时间优化 (LTO)?

c++ - 如何开始使用 Visual C++ 编写 DLL?

c++ - 访问单例类 C++ 时遇到问题

c++ - 具有虚拟析构函数的池分配器

c++ - enable_if + is_same + constexpr 函数使 MSVC 失败(但在 Clang、GCC 中运行良好)

c++ - 为什么未使用的参数不会被编译器丢弃?