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

时间:2010-03-29 00:15:41

标签: c++ c volatile compiler-optimization

编译器无法消除或重新排序对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;

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

4 个答案:

答案 0 :(得分:12)

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

  

可观察到的行为   抽象机是它的序列   读取和写入易失性数据和   调用库I / O函数。

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

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

  

......符合要求的实施方案   需要模仿(仅)   抽象的可观察行为   机器...

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

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

答案 1 :(得分:4)

对于方案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 ++的易失性分别比有序原子更少限制性和限制性更强。一些编译器厂商支持这种解释;其他人根本不优化易失性读取或写入;还有一些人有自己喜欢的语义。

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

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

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

  •   
     

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

答案 2 :(得分:2)

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

答案 3 :(得分:0)

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

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

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

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