C++内存模型可以合并原子加载吗?
Can atomic loads be merged in the C++ memory model?
考虑下面的 C++ 11 代码段。对于 GCC 和 clang,这会编译为两个(顺序一致的)foo 负载。 (编者注:编译器不优化原子,请参阅 for more details, especially http://wg21.link/n4455 标准关于这可能产生的问题的讨论,标准没有为程序员提供解决问题的工具。这个语言律师问答是关于当前标准的,而不是什么编译器会。)
C++ 内存模型是否允许编译器将这两个加载合并为一个加载并对 x 和 y 使用相同的值?
(编者注:这是标准组正在研究的内容:http://wg21.link/n4455 and http://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 的线程,您可以保证 y
将具有相同的 或 与 x
.
的内容比较时写入的值
// 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)
}
想象一下,由于某种原因,写入线程在每次迭代时暂停执行(由于 上下文切换 或其他实现定义的原因);您无法证明这是导致 x
和 y
具有相同值的原因,或者是因为 "merge optimization".
换句话说,我们必须根据本节中的代码得出可能的结果:
- 在两次读取 (
x == y
) 之间没有新值写入 foo。
- 在两次读取 (
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
?
答案;号
在顺序一致的环境中,我们保证在 线程 2 中对 foo 的写入将在 中可见]线程 1;这意味着当写入发生时,线程 1 必须观察到这种状态变化。
注意:一个实现可以将两个读取变成一个读取,因为我们无法观察到差异(一个fence与2),但它不能完全忽视一个单独存在的读。
注意:本段内容由[atomics.order]p3-4
保证。
如果我真的想阻止这种形式的"optimization"怎么办?
如果你想强制实现在你编写它的每个点实际读取某个变量的值,你应该查看 volatile
的用法(注意这在没有办法增强线程安全性)。
但实际上编译器不会优化原子,并且标准组建议不要出于这种原因使用volatile atomic
,直到尘埃落定为止.参见
- http://wg21.link/n4455
- http://wg21.link/p0062
- 和这个问题的副本,
是的,在您的特定示例中(没有其他情况)。
您的特定示例具有单个执行线程,foo
具有静态存储持续时间和初始化(即,在输入 main
之前),并且在程序的生命周期内永远不会被修改。
换句话说,没有外部可观察到的差异,并且可以合法地应用假设规则。事实上,编译器可以完全取消原子指令。 x
或 y
的值永远不可能有任何不同。
在具有修改 foo
的并发程序中,情况并非如此。
您没有指定内存模型,所以使用默认模型,即顺序一致性。顺序一致性被定义为提供与 release/acquire 相同的发生前/内存排序保证,并建立所有原子操作的单一总修改顺序。 最后一位是重要部分.
单个总修改顺序意味着如果您有三个(原子)操作,例如A、B 和 C 按此顺序发生(可能在两个线程中同时发生),B 是写操作,而 A 和 C 是读操作,则 C 必须 看到状态由 B 而不是其他更早的状态建立。也就是说,在 A 点和 C 点看到的值将不同。
就您的代码示例而言,如果另一个线程在您将 foo
读入 x
之后立即修改它(但在您将值读入 y
之前),则放入 y
中的值必须 是写入的值。因为如果操作按那个顺序发生,它们也必须按那个顺序实现。
当然,写入恰好发生在两个连续的加载指令之间是不太可能的事情(因为时间 window 非常小,只有一个滴答),但它是否无关紧要不太可能。
编译器必须生成代码,以确保如果出现这种情况,操作仍会按照它们发生的顺序显示。
考虑下面的 C++ 11 代码段。对于 GCC 和 clang,这会编译为两个(顺序一致的)foo 负载。 (编者注:编译器不优化原子,请参阅
C++ 内存模型是否允许编译器将这两个加载合并为一个加载并对 x 和 y 使用相同的值?
(编者注:这是标准组正在研究的内容:http://wg21.link/n4455 and http://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 的线程,您可以保证 y
将具有相同的 或 与 x
.
// 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)
}
想象一下,由于某种原因,写入线程在每次迭代时暂停执行(由于 上下文切换 或其他实现定义的原因);您无法证明这是导致 x
和 y
具有相同值的原因,或者是因为 "merge optimization".
换句话说,我们必须根据本节中的代码得出可能的结果:
- 在两次读取 (
x == y
) 之间没有新值写入 foo。 - 在两次读取 (
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 tofoo
?
答案;号
在顺序一致的环境中,我们保证在 线程 2 中对 foo 的写入将在 中可见]线程 1;这意味着当写入发生时,线程 1 必须观察到这种状态变化。
注意:一个实现可以将两个读取变成一个读取,因为我们无法观察到差异(一个fence与2),但它不能完全忽视一个单独存在的读。
注意:本段内容由[atomics.order]p3-4
保证。
如果我真的想阻止这种形式的"optimization"怎么办?
如果你想强制实现在你编写它的每个点实际读取某个变量的值,你应该查看 volatile
的用法(注意这在没有办法增强线程安全性)。
但实际上编译器不会优化原子,并且标准组建议不要出于这种原因使用volatile atomic
,直到尘埃落定为止.参见
- http://wg21.link/n4455
- http://wg21.link/p0062
- 和这个问题的副本,
是的,在您的特定示例中(没有其他情况)。
您的特定示例具有单个执行线程,foo
具有静态存储持续时间和初始化(即,在输入 main
之前),并且在程序的生命周期内永远不会被修改。
换句话说,没有外部可观察到的差异,并且可以合法地应用假设规则。事实上,编译器可以完全取消原子指令。 x
或 y
的值永远不可能有任何不同。
在具有修改 foo
的并发程序中,情况并非如此。
您没有指定内存模型,所以使用默认模型,即顺序一致性。顺序一致性被定义为提供与 release/acquire 相同的发生前/内存排序保证,并建立所有原子操作的单一总修改顺序。 最后一位是重要部分.
单个总修改顺序意味着如果您有三个(原子)操作,例如A、B 和 C 按此顺序发生(可能在两个线程中同时发生),B 是写操作,而 A 和 C 是读操作,则 C 必须 看到状态由 B 而不是其他更早的状态建立。也就是说,在 A 点和 C 点看到的值将不同。
就您的代码示例而言,如果另一个线程在您将 foo
读入 x
之后立即修改它(但在您将值读入 y
之前),则放入 y
中的值必须 是写入的值。因为如果操作按那个顺序发生,它们也必须按那个顺序实现。
当然,写入恰好发生在两个连续的加载指令之间是不太可能的事情(因为时间 window 非常小,只有一个滴答),但它是否无关紧要不太可能。
编译器必须生成代码,以确保如果出现这种情况,操作仍会按照它们发生的顺序显示。