16

编译器无法消除或重新排序对volatile-qualified 变量的读/写。

但是在其他变量存在的情况下,可能是也可能不是- 限定的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

4 回答 4

13

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

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

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

在方案 2 中,您提出的任何更改都不会更改顺序。所以他们在“as-if”规则(1.9/1)下是允许的:

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

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

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

于 2010-03-29T00:51:14.160 回答
4

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

  • 以及bandc变量在当前函数之外是否可见(通过非本地或已传递其地址)
  • 您与谁交谈(显然对于字符串volatile在 C/C++ 中的使用方式存在一些分歧)
  • 你的编译器实现

所以(软化我的第一个答案),我想说,如果你依赖于场景 2 中的某些行为,你必须将其视为不可移植代码,其在特定平台上的行为将由实现的文档可能会表明(如果文档对此只字未提,那么您对有保证的行为就不走运了。

从 C99 5.1.2.3/2“程序执行”开始:

访问 volatile 对象、修改对象、修改文件或调用执行任何这些操作的函数都是副作用,它们是执行环境状态的变化。表达式的评估可能会产生副作用。在被称为序列点的执行序列中的某些指定点,之前评估的所有副作用都应该是完整的,并且后续评估的副作用应该没有发生。

...

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

  • 在序列点,易失性对象是稳定的,因为先前的访问已完成,而后续的访问尚未发生。

volatile以下是 Herb Sutter 对C/C++ 中访问所需行为的一些看法(来自“volatilevolatilehttp://www.ddj.com/hpc-high-performance-computing/212701484):

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

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

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

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

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

于 2010-03-29T00:44:09.030 回答
2

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

于 2010-03-29T00:45:09.870 回答
0

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

编写为需要栅栏的代码,在提供栅栏的平台上运行,可能比编写为不需要栅栏的代码运行得更好,在不提供栅栏的平台上运行,但如果不提供栅栏,需要栅栏的代码将出现故障. 不需要栅栏的代码在提供它们的平台上通常比需要栅栏的代码运行得更慢,并且提供栅栏的实现将比那些不需要栅栏的实现更慢地运行此类代码。

一个好的方法可能是将宏定义为在意味着内存栅栏的semi_volatile系统上扩展为空,或者在没有内存栅栏的系统上扩展为空。如果需要相对于其他变量而不是彼此进行访问排序的变量被限定为,并且该宏定义正确,则在有或没有内存围栏的系统上都将实现可靠的操作,并且可以是最有效的操作将在带有围栏的系统上实现。如果编译器实际上实现了一个按要求工作的限定符,则可以将其定义为使用该限定符并获得更好代码的宏。volatilevolatilevolatilesemi-volatilesemivolatile

IMHO, that's an area the Standard really should address, since the concepts involved are applicable on many platforms, and any platform where fences aren't meaningful can simply ignore them.

于 2016-07-08T00:22:19.443 回答