“volatile”限定符和编译器重新排序

编译器不能消除或重新排序对volatile限定变量的读/写。

但是,存在其他变量的情况又如何,可能是也可能不是volatile

情景1

 volatile int a; volatile int b; a = 1; b = 2; a = 3; b = 4; 

编译器可以重新排序第一个和第二个,第三个和第四个赋值吗?

场景2

 volatile int a; int b, c; b = 1; a = 1; c = b; a = 3; 

同样的问题,编译器可以重新排序第一个和第二个,或第三个和第四个分配?

C ++标准说(1.9 / 6):

抽象机器的可观察行为是它对易失性数据的读写顺序以及对库I / O函数的调用。

在方案1中,您建议的任何更改都会更改对易失性数据的写入顺序。

在方案2中,您建议的更改都不会更改序列。 所以他们被允许遵循“as-if”规则(1.9 / 1):

…符合实现需要模拟(仅)抽象机器的可观察行为……

为了说明这种情况已经发生,您需要检查机器代码,使用调试器,或引发未定义或未指定的行为,这些行为的结果是您在实现时所知道的。 例如,实现可能会保证同时执行的线程具有相同内存的视图,但这超出了C ++标准的范围。 因此,虽然标准可能允许特定的代码转换,但是特定的实现可以排除它,理由是它不知道您的代码是否将在multithreading程序中运行。

如果您要使用可观察行为来测试重新排序是否已经发生(例如,在上面的代码中打印变量的值),那么当然标准不允许这样做。

对于方案1,编译器不应执行您提到的任何重新排序。 对于方案2,答案可能取决于:

  • 以及bc变量是否在当前函数之外是可见的(通过非本地或已经传递其地址)
  • 你跟谁说话(显然对C / C ++中的字符串volatile存在一些分歧)
  • 你的编译器实现

所以(软化我的第一个答案),我会说,如果你依赖于场景2中的某些行为,你必须将它视为非可移植代码,它在特定平台上的行为将由任何实现的文档可能表明(如果文档没有说明它,那么你就不能保证行为。

来自C99 5.1.2.3/2“程序执行”:

访问易失性对象,修改对象,修改文件或调用执行任何这些操作的函数都是副作用,这些都是执行环境状态的变化。 表达的评估可能产生副作用。 在称为序列点的执行序列中的某些特定点处,先前评估的所有副作用应是完整的,并且不会发生后续评估的副作用。

(第5段)对符合要求的实施的最低要求是:

  • 在序列点处,易失性对象在先前访问完成且后续访问尚未发生的意义上是稳定的。

以下是Herb Sutter关于C / C ++中volatile访问所需行为的一些内容(来自“ volatile vs. volatilehttp://www.ddj.com/hpc-high-performance-computing/212701484 ):

附近的普通读写怎么样 – 那些仍然可以在不可优化的读写中重新排序? 今天,没有实用的可移植答案,因为C / C ++编译器的实现差别很大,不太可能很快收敛。 例如,对C ++标准的一种解释认为,普通读取可以在C / C ++易失性读或写的任一方向上自由移动,但是普通的写入不能在C / C ++易失性读或写中移动 – 会使C / C ++的易失性分别比有序primefaces更具限制性和限制性。 一些编译器厂商支持这种解释; 其他人根本不优化易失性读取或写入; 还有一些人有自己喜欢的语义。

为了它的价值,Microsoft为C / C ++ volatile关键字(如Microsoft-sepcific)记录了以下内容:

  • 对volatile对象的写入(volatile write)具有Release语义; 对在指令序列中写入易失性对象之前发生的全局或静态对象的引用将在编译二进制文件中的易失性写入之前发生。

  • 读取volatile对象(volatile read)具有Acquire语义; 在读取指令序列中的易失性存储器之后发生的对全局或静态对象的引用将在编译二进制文件中的易失性读取之后发生。

这允许volatile对象用于multithreading应用程序中的内存锁定和释放。

易失性不是记忆围栏。 可以消除或执行片段#2中对B和C的分配。 为什么你希望#2中的声明导致#1的行为?

一些编译器将对volatile限定对象的访问视为内存栅栏。 其他人没有。 编写一些程序要求volatile作为栅栏。 其他人不是。

编写为需要在提供它们的平台上运行的围栏的代码可能比编写为不需要围栏的代码运行得更好,在不提供它们的平台上运行,但是如果没有提供围栏则需要围栏的代码会出现故障。 不需要围栏的代码在提供它们的平台上运行速度通常比需要围栏的代码运行得慢,而提供围栏的实现将比不需要围栏的代码运行更慢。

一个好的方法可能是将一个宏semi_volatile定义为在volatile表示内存栅栏的系统上扩展为semi_volatile ,或者在不包含内存栅栏的系统上扩展为volatile 。 如果需要对其他volatile变量进行有序访问的变量被认定为semi-volatile ,并且该宏被正确定义,那么在具有或不具有内存栅栏的系统上将实现可靠的操作,并且效率最高将实现可以在带栅栏的系统上实现的操作。 如果编译器实际上实现了一个按要求工作的限定符,那么它可以定义为使用该限定符并实现更好代码的宏。

恕我直言,这是标准真正应该解决的问题,因为所涉及的概念适用于许多平台,任何围栏无意义的平台都可以忽略它们。