是否保证保留对 volatile 结构的单独成员的写入顺序?

Ted*_*elt 33 c c++ concurrency volatile language-lawyer

假设我有一个这样的结构:

volatile struct { int foo; int bar; } data;
data.foo = 1;
data.bar = 2;
data.foo = 3;
data.bar = 4;
Run Code Online (Sandbox Code Playgroud)

是否保证所有分配都不会重新排序?

例如,如果没有 volatile,编译器显然可以将它优化为两个不同顺序的指令,如下所示:

data.bar = 4;
data.foo = 3;
Run Code Online (Sandbox Code Playgroud)

但是对于 volatile,是否要求编译器不要做这样的事情?

data.foo = 1;
data.foo = 3;
data.bar = 2;
data.bar = 4;
Run Code Online (Sandbox Code Playgroud)

(将成员视为单独的不相关的易失性实体 - 并进行重新排序,我可以想象它可能会尝试改善引用的局部性,以防foobar位于页面边界 - 例如。)

此外,答案是否与 C 和 C++ 标准的当前版本一致?

Nat*_*dge 30

它们不会被重新排序。

C17 6.5.2.3(3) 说:

后缀表达式后跟 . 运算符和标识符指定结构或联合对象的成员。该值是命名成员的值,97),如果第一个表达式是左值,则是左值。如果第一个表达式具有限定类型,则结果具有指定成员类型的如此限定版本。

由于data具有volatile- 限定类型,所以做data.barand data.foo。因此,您正在对volatile int对象执行两个分配。通过 6.7.3 脚注 136,

对如此声明为 [as volatile] 的对象的操作不应被实现“优化掉”或重新排序,除非评估表达式的规则允许。

一个更微妙的问题是编译器是否可以用一条指令将它们分配给它们,例如,如果它们是连续的 32 位值,它是否可以使用 64 位存储来设置两者?我认为不会,至少 GCC 和 Clang 不会尝试这样做。

  • 它是实现定义的,什么构成了对易失性限定对象的访问。如果 C 实现的目标硬件上 64 位写入的效果可能与两个 32 位写入的效果相同(例如,共享内存的其他组件可能会单独看到两个 32 位写入,但它们可以被视为无法区分,因此必须同时进行的 64 位写入与实际上同时发生的两个 32 位写入无法区分),那么实现定义“访问”是合理的,以便可以进行 64 位写入用过的。 (3认同)
  • @TedShaneyfelt:结构类型的成员本身就是对象。6.2.5 (20):“结构类型描述了按顺序分配的非空成员**对象**集”。所以我们确实对易失性对象执行了两次访问,它们恰好是不同的对象,尽管它们也是对象“data”的一部分。我更改了措辞,以明确表示即使对同一对象进行两次访问,仍然会禁止重新排序(当前情况并非如此)。 (2认同)

Sum*_*uma 17

如果你想在多个线程中使用它,有一个重要的问题。

虽然编译器不会对volatile变量的写入重新排序(如Nate Eldredge 的回答所述),但还有一点可以发生写入重新排序,那就是 CPU 本身。这取决于 CPU 架构,下面举几个例子:

英特尔 64

请参阅英特尔® 64 位架构内存订购白皮书

虽然商店指令本身没有重新排序(2.2):

  1. 商店不会与其他商店重新排序。

它们可能以不同的顺序对不同的 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 变量甚至可能无法用作线程间标志或互斥锁.