原子负载是否可以合并到C++内存模型中
Can atomic loads be merged in the C++ memory model?
请考虑下面的C++ 11 片段。对于 GCC 和 clang,这会编译为两个(顺序一致的(foo负载。 (编者注:编译器不会优化原子学,请参阅此问答以获取更多详细信息,特别是 http://wg21.link/n4455 标准讨论,讨论这可能产生的问题,而标准没有为程序员提供解决方法。 这个语言律师问答是关于当前标准,而不是编译器所做的。
C++内存模型是否允许编译器将这两个加载合并为单个加载,并对 x 和 y 使用相同的值?
(编者注:这是标准组织正在做的事情:http://wg21.link/n4455 和 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 %dn", x, y);
return 0;
}
是的,因为我们无法观察到差异!
允许实现将您的代码段转换为以下内容(伪实现(。
int __loaded_foo = foo;
int x = __loaded_foo;
int y = __loaded_foo;
原因是您无法观察到上述内容与给定顺序一致性保证的两个单独的 foo 负载之间的差异。
注意:不仅仅是编译器可以进行这样的优化,处理器可以简单地推断出没有办法观察差异并加载一次
foo
的值 - 即使编译器可能要求它这样做两次。
解释
给定一个不断以增量方式更新 foo 的线程,您可以保证的是,与 x
的内容相比,y
将具有相同或更晚的写入值。
// 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
具有相同值的原因,或者是否是因为"合并优化"。
换句话说,我们必须给定本节中的代码的潜在结果:
- 在两次读取之间没有向 foo 写入新值 (
x == y
(。 - 在两个读取(
x < y
(之间写入一个新值到foo。
由于两者中的任何一个都可能发生,因此实现可以自由地缩小范围,以始终执行其中之一;我们绝不能观察到其中的区别。
《标准》怎么说?
实现可以进行它想要的任何更改,只要我们看不到我们表达的行为与执行期间的行为之间的任何差异。
这在[intro.execution]p1
中有所介绍:
本标准中的语义描述定义了 参数化非确定性抽象机。这个国际 标准对符合性结构没有要求 实现。特别是,他们不需要复制或模仿 抽象机器的结构。相反,符合实现 需要(仅(模拟抽象的可观察行为 机器如下所述。
另一部分使其更加清晰[intro.execution]p5
:
执行格式良好的程序的符合实现应 产生与其中一个可能的执行相同的可观察行为 具有相同功能的抽象机器的相应实例 程序和相同的输入。
延伸阅读:
- 究竟什么是"假设"规则"?
循环轮询呢?
// initial state
std::atomic<int> foo = 0;
// thread 1
while (true) {
if (foo)
break;
}
// thread 2
foo = 1
问题:鉴于前面部分中的推理,实现是否可以简单地在线程 1 中读取一次
foo
,然后即使线程 2 写入foo
也永远不会脱离循环?
答案;不。
在顺序一致的环境中,我们保证线程 2 中对 foo 的写入将在线程 1 中可见;这意味着当写入发生时,线程 1 必须观察这种状态变化。
注意:实现可以将两个读取转换为单个读取,因为我们无法观察到差异(一个栅栏与两个栅栏一样有效(,但它不能完全忽略本身存在的读取。
注意:本节的内容由 [atomics.order]p3-4
保证。
如果我真的想阻止这种形式的"优化"怎么办?
如果你想强制实现在你编写它的每个点实际读取某个变量的值,你应该研究volatile
的用法(请注意,这绝不会增强线程安全性(。
但在实践中,编译器不会优化原子学,标准组织建议不要出于这种原因使用volatile atomic
,直到这个问题尘埃落定。 看
- http://wg21.link/n4455
- http://wg21.link/p0062
- 为什么编译器不合并冗余的 std::atomic writes?
- 以及这个问题的副本,编译器可以并且是否优化了两个原子负载?
是的,在您的特定示例中(没有其他方法(。
您的特定示例具有单个执行线程,foo
具有静态存储持续时间和初始化(即,在输入main
之前(,否则在程序的生命周期内永远不会修改。
换句话说,没有外部可观察到的差异,并且可以合法地应用原样规则。事实上,编译器可以完全取消原子指令。x
或y
的价值不可能有任何不同。
在修改foo
的并发程序中,情况并非如此。
您没有指定内存模型,因此使用默认模型,即顺序一致性。顺序一致性定义为提供与释放/获取相同的发生前/内存排序保证,并建立所有原子操作的单个总修改顺序。最后一点是重要的部分。
单个总修改顺序意味着,如果你有三个(原子(操作,例如 A、B 和 C,它们按该顺序发生(可能同时发生在两个线程中(,并且 B 是写入操作,而 A 和 C 是读取操作,那么 C 必须看到 B 建立的状态,而不是其他一些早期状态。也就是说,在点 A 和 C 处看到的值将不同。
就代码示例而言,如果另一个线程在您将其读入x
之后(但在将值读入y
之前(立即修改foo
,则放入y
的值必须是写入的值。因为如果操作按该顺序发生,则也必须按该顺序实现。
当然,恰好在两个连续加载指令之间发生的写入是一件不太可能的事情(因为时间窗口非常小,只是一个刻度(,但是否不太可能并不重要。
编译器必须生成代码,以确保如果出现此星座,操作仍会完全按照其发生的顺序查看。
- 将字符串存储在c++中的稳定内存中
- C++ 指针的内存地址和指向数组的内存地址如何相同?
- Win32编译器选项和内存分配
- 当vector是tje全局变量时,c++中vector的内存管理
- 带内存和隔离功能的SQLite
- 是否可以通过C++扩展强制多个python进程共享同一内存
- 迭代时从向量和内存中删除对象
- 在C++中打印指向不同基元数据类型的指针的内存地址
- 这个指针和内存代码打印是什么?我不知道是打印垃圾还是如何打印我需要的值
- 多个文件的内存分配错误"在抛出 'std :: bad_alloc' what (): std :: bad_alloc 的实例后终止调用" [C++]
- 为什么示例代码访问IUnknown中已删除的内存
- 如何在C++类内存结构中创建"spacer"?
- 从构造函数抛出异常时如何克服内存泄漏
- malloc() 可能出现内存泄漏
- 如何理解将半精度指针转换为无符号长指针和相关的内存对齐
- 在调用FreeLibrary后,释放动态链接到具有相同版本的CRT堆的DLL的内存
- 如何针对特定情况调试和修复此双自由内存损坏问题
- 类型总是使用其大小存储在内存中吗
- 有没有一种方法可以测量c++程序的运行时内存使用情况
- 有没有一种方法可以使用placement new将堆叠对象分配给分配的内存