是否可以重新订购对挥发物的访问?

Kno*_*abe 11 c++ volatile c++11

考虑以下对volatile内存的写入顺序,我从David Chisnall在InformIT的文章 "理解C11和C++ 11 Atomics"中获取:

volatile int a = 1;
volatile int b = 2;
             a = 3;
Run Code Online (Sandbox Code Playgroud)

我对C++ 98的理解是,根据C++ 98 1.9,这些操作无法重新排序:

符合实现需要模拟(仅)抽象机器的可观察行为,如下所述...抽象机器的可观察行为是它对易失性数据的读写顺序和对库I/O函数的调用

Chisnall说,对订单保存的约束仅适用于单个变量,写出符合要求的实现可以生成执行此操作的代码:

a = 1;
a = 3;
b = 2;
Run Code Online (Sandbox Code Playgroud)

或这个:

b = 2;
a = 1;
a = 3;
Run Code Online (Sandbox Code Playgroud)

C++ 11重复了C++ 98的措辞

符合实现需要模拟(仅)抽象机器的可观察行为,如下所述.

但这说关于volatiles(1.9/8):

严格根据抽象机器的规则来评估对volatile对象的访问.

1.9/12表示访问volatileglvalue(包括变量a,bc以上)是副作用,1.9/14表示一个完整表达式(例如,一个语句)中的副作用必须先于后面的副作用在同一个线程中完整表达.这使我得出结论,Chisnall显示的两个重新排序是无效的,因为它们不符合抽象机器所规定的排序.

我忽略了什么,还是Chisnall错了?

(请注意,这不是一个线程问题.问题是是否允许编译器重新排序对volatile单个线程中不同变量的访问.)

Joe*_*rgB 5

IMO Chisnalls的解释(由你提出)显然是错误的.更简单的情况是C++ 98.将sequence of reads and writes to volatile data要保存需求,并适用于有序序列读取和写入任何挥发性的数据,而不是一个变量.

如果你考虑volatile的最初动机:内存映射I/O,这就变得很明显了.在mmio中,您通常在不同的存储器位置有几个相关的寄存器,并且I/O设备的协议需要对其寄存器组进行特定的读写操作序列 - 寄存器之间的顺序很重要.

C++ 11的措辞避免讨论绝对sequence of reads and writes,因为在多线程环境中,跨线程没有一个明确定义的此类事件序列 - 如果这些访问转到独立的内存位置,这不是问题.但我相信,对于任何具有良好定义顺序的易失性数据访问序列,规则保持与C++ 98相同 - 无论在该序列中访问多少个不同位置,都必须保留顺序.

这是一个完全独立的问题,需要实现.如何(甚至可以)从程序外部观察易失性数据访问以及程序的访问顺序如何映射到外部可观察事件是未指定的.实施应该可以给你一个合理的解释和合理的保证,但合理的取决于上下文.

C++ 11标准为非同步的易失性访问之间的数据竞争留下了空间,因此没有任何东西需要通过完整的内存栅栏或类似的结构来围绕它们.如果存储器的某些部分真正用作外部接口 - 用于存储器映射的I/O或DMA - 则实现可能是合理的,可以保证对这些部件的易失性访问如何暴露给消耗设备.

可以从标准中推断出一种保证(参见[into.execution]):volatile std::sigatomic_t即使在信号处理程序中,类型的值必须具有与写入顺序兼容的值 - 至少在单线程程序中.


Jon*_*ely 5

你是对的,他是错的。对不同易失性变量的访问不能由编译器重新排序,只要它们出现在单独的完整表达式中,即由 C++98 所谓的序列点分隔,或者用 C++11 术语来说,一个访问先于另一个访问排序。

Chisnall 似乎试图解释为什么volatile对于编写线程安全代码来说毫无用处,他展示了一个简单的互斥实现,volatile该实现依赖于编译器重新排序会破坏的互斥实现。他是对的,这volatile对于线程安全来说毫无用处,但并不是因为他给出的原因。这并不是因为编译器可能会重新排序对volatile对象的访问,而是因为 CPU 可能会重新排序它们。原子操作和内存屏障会阻止编译器CPU 根据线程安全的需要跨屏障重新排序。

请参阅 Sutter 的信息丰富的“挥发性与挥发性”文章中表 1 右下角的单元格。

  • 从 C++ 标准的角度来看,编译器执行某些操作与编译器发出导致硬件执行某些操作的指令之间没有区别。如果 CPU 可以重新排序对易失性的访问,则标准不要求保留它们的顺序。不可能是双向的。 (2认同)
  • C++ 标准对于重新排序的内容没有任何区别。而且你不能争辩说 CPU 可以对它们重新排序而没有可观察到的影响,所以没关系——C++ 标准*定义*它们的顺序是可观察的。如果编译器生成的代码使平台执行标准要求的操作,则该编译器符合平台上的 C++ 标准。如果标准要求对易失性的访问不进行重新排序,则对它们进行重新排序的平台不符合要求。这里没有假设规则——定义的顺序是可观察的。 (2认同)
  • @David您忽略的是c ++标准指定了仅在特定情况下交互的多个线程的行为,而其他一切都会导致未定义的行为。如果不使用原子变量,则涉及至少一次写入的竞争条件是未定义的,因此编译器完全有权利放弃任何 cpu 同步,因为您只会注意到表现出未定义行为的程序中的差异。 (2认同)