是否保证保留对volatile结构的单独成员的写入顺序?
假设我有一个这样的结构:
volatile struct { int foo; int bar; } data;
data.foo = 1;
data.bar = 2;
data.foo = 3;
data.bar = 4;
是否保证所有分配都不会重新排序?
例如,如果没有 volatile,编译器显然可以将它优化为两个不同顺序的指令,如下所示:
data.bar = 4;
data.foo = 3;
但是对于 volatile,是否要求编译器不要做这样的事情?
data.foo = 1;
data.foo = 3;
data.bar = 2;
data.bar = 4;
(将成员视为单独的不相关的易失性实体 - 并进行重新排序,我可以想象它可能会尝试改善引用的局部性,以防foo和bar位于页面边界 - 例如。)
此外,答案是否与 C 和 C++ 标准的当前版本一致?
回答
C
它们不会被重新排序。
C17 6.5.2.3(3) 说:
后缀表达式后跟 . 运算符和标识符指定结构或联合对象的成员。该值是命名成员的值,97),如果第一个表达式是左值,则是左值。如果第一个表达式具有限定类型,则结果具有指定成员类型的如此限定版本。
由于data
具有volatile
- 限定类型,所以做data.bar
and data.foo
。因此,您正在对volatile int
对象执行两个分配。通过 6.7.3 脚注 136,
对如此声明为 [as
volatile
] 的对象的操作不应被实现“优化掉”或重新排序,除非评估表达式的规则允许。
一个更微妙的问题是编译器是否可以用一条指令将它们分配给它们,例如,如果它们是连续的 32 位值,它是否可以使用 64 位存储来设置两者?我认为不会,至少 GCC 和 Clang 不会尝试这样做。
- It is implementation-defined what constitutes access to a volatile-qualified object. If the C implementation targets hardware on which the effects of a 64-bit write could be the same as two 32-bit writes (e.g., two 32-bit writes might be seen separately by other components sharing memory, but they could be seen as indistinguishable, so a 64-bit write that is necessarily simultaneous is indistinguishable from two 32-bit writes that happen to be effectively simultaneous), then it could be reasonable for the implementation to define “access” so that a 64-bit write can be used.
回答
如果你想在多个线程中使用它,有一个重要的问题。
虽然编译器不会对volatile
变量的写入重新排序(如Nate Eldredge 的回答中所述),但还有一点可以发生写入重新排序,那就是 CPU 本身。这取决于 CPU 架构,下面举几个例子:
英特尔 64
请参阅英特尔® 64 位架构内存订购白皮书。
虽然商店指令本身没有重新排序(2.2):
- 商店不会与其他商店重新排序。
它们可能以不同的顺序对不同的 CPU 可见 (2.4):
英特尔 64 位内存排序允许这两个处理器以不同的顺序查看两个处理器的存储
AMD 64
AMD 64(这是常见的 x64)在规范中具有类似的行为:
通常,不允许乱序写入。在所有先前的指令按照程序顺序完成之前,乱序执行的写入指令无法将其结果提交(写入)到内存中。但是,处理器可以将乱序写入指令的结果保存在私有缓冲区(软件不可见)中,直到该结果可以提交到内存。
电脑
我记得在使用 PowerPC CPU 的 Xbox 360上必须小心这一点:
虽然 Xbox 360 CPU 不会对指令重新排序,但它会重新排列写操作,这些操作会在指令本身之后完成。这种写入的重新排列是 PowerPC 内存模型特别允许的
为了避免以可移植的方式重新排序 CPU,您需要使用内存栅栏,如 C++11 std::atomic_thread_fence或 C11 atomic_thread_fence。没有它们,从另一个线程看到的写入顺序可能会有所不同。
另请参见C++11 引入了标准化的内存模型。这是什么意思?它将如何影响 C++ 编程?
维基百科内存屏障文章中也提到了这一点:
此外,由于缓存、缓存一致性协议和宽松的内存排序,无法保证其他处理器或内核会以相同的顺序看到 volatile 读取和写入,这意味着单独的 volatile 变量甚至可能无法用作线程间标志或互斥锁.