指向非volatile对象的volatile指针行为要求

13

C11 6.7.3类型限定符第7段规定:

 

具有volatile限定类型的对象可能会以实现未知或具有其他未知副作用的方式进行修改。因此,任何引用此类对象的表达式都应严格按照5.1.2.3中描述的抽象机器规则进行评估。

在下面的示例中,第三行访问的对象是否受上述规则的约束?

int x;
volatile int *p = &x;
*p = 42;

换句话说,lvalue *p 的类型为volatile int是否意味着正在访问一个volatile对象,还是p指向的非volatile对象x意味着编译器可以利用这个信息进行优化并省略volatile访问?

由于它可能很有趣,我感兴趣的特定用例超出了普通C的范围;它涉及使用pre-C11构造进行原子线程同步(可能是内联汇编或仅被视为黑匣子)进行原子比较和交换,采用以下惯用法:

do {
    tmp = *p;
    new = f(tmp);
} while (atomic_cas(p, tmp, new) != success);

这里指针p的类型将为volatile int *,但我担心指向的实际对象不是volatile时会发生什么,特别是编译器是否会将对*p的单个访问转换为以下两个访问形式:

do {
    new = f(*p);
} while (atomic_cas(p, *p, new) != success);

很明显,这会导致代码不正确。因此,目标是确定所有这样指向的对象是否实际需要是volatile int


2
符合规范的程序无法检测到差异,因为访问和修改 x 不是可观察的副作用。因此,编译器可以根据 as-if 规则进行优化。 - Igor Tandetnik
2
如果硬件访问x并且您将其声明为非易失性,则您的硬件配置未实现C虚拟机。 - philipxy
1
在许多系统中,某些对象将在编译器之外的特定可识别时间受到操作。允许编译器在除了可能被外部因素更改的那些时间之外,使用正常的语义处理这些对象,将使代码比如果需要一直使用 volatile 语义进行处理更加高效。我认为,在“优化”的名义下,要求程序员防止编译器生成高效代码的想法是荒谬的。 - supercat
1
@supercat,我不认为我理解你试图表达的观点;问题定义了一个非易失性int x,但您正在谈论一个已定义为易失性的对象。对于声明extern int x,标准说:“如果尝试通过使用具有非易失性限定类型的lvalue引用已定义为易失性限定类型的对象,则行为未定义。”标准不支持您的兼职易失性。 - philipxy
1
如果编译器支持内存屏障,并且代码可以通过某种方式表明它使用这些屏障而不是仅依赖于volatile,那么最好使用这些屏障而不是依赖于volatile。另一方面,为了适应保守处理volatile的编译器而设计的代码将自然地在其他这样的编译器上工作,而无需明确地适应任何一个编译器。 - supercat
显示剩余9条评论
2个回答

9

更新 2017年2月18日

下面的答案引用并讨论了标准中的语言,理性中一些矛盾的语言以及gnu.cc的一些评论。有一个缺陷报告基本上得到了委员会的同意(尽管仍然保持着开放状态),即标准应该说,并且一直都是这样的意图,并且实现一直反映出来,即不是对象的易变性很重要(按照标准),而是(访问的lvalue)的易变性很重要(按照理性)。 (感谢Olaf提到了这个DR。)

C11版本1.10的缺陷报告摘要 日期:2016年4月 DR 476 volatile semantics for lvalues 04/2016 Open


不行。因为所访问的对象不是易失性的。

对象p是指向易失性int的指针类型。但是x不是易失性限定类型的对象。 p上的资格影响可以通过它进行的访问,但不影响它所指向的对象的类型。在通过易失性lvalue访问未限定类型对象上没有限制。因此,通过p访问x不是访问易失性限定类型对象。

(请参见6.7.3类型限定符,以了解访问限定类型对象的限制。它只是说您不能通过未限定的lvalue访问易失性限定对象。)

另一方面,这篇文章引用了国际标准-编程语言-C的理性6.7.3:

将值转换为限定类型的强制转换没有效果;资格(例如易失性)对访问没有影响,因为它已经发生在案例之前。如果需要使用易失性语义访问非易失性对象,则技术是将对象的地址转换为适当的限定类型指针,然后取消引用该指针。

然而,我找不到标准中说明语义基于lvalue类型的语言。从gnu.org

有一点混淆的区别是使用 volatile 类型定义的对象和 volatile 左值之间的区别。从 C 标准的角度来看,使用 volatile 类型定义的对象具有外部可见行为。你可以将这样的对象视为附加了示波器探头,以便用户观察访问它们的某些属性,就像用户可以观察写入输出文件的数据一样。然而,标准并没有明确规定用户是否可以观察由 volatile 左值对普通对象的访问。从标准上来看,如果底层对象是普通对象,则不清楚 volatile 左值是否提供比非 volatile 左值更多的保证。
不会,因为没有副作用。即使 *p 的语义必须是 volatile,标准仍然说:在抽象机中,所有表达式都按照语义规定进行评估。如果实际实现可以推断出其值未被使用且不产生所需的副作用(包括通过调用函数或访问 volatile 对象引起的任何副作用),则无需评估表达式的一部分。同样,在您的代码中没有 volatile 对象。虽然只能看到 p 的编译单元无法进行该优化。
还要记住的是,对具有 volatile 限定类型的对象进行访问的定义是实现定义的。程序执行时更严格的抽象和实际语义对应关系可以由每个实现定义。因此,仅有 volatile 左值的外观并不能告诉您哪些“访问”。除非文档化的实现行为,否则您无权谈论“从 tmp = *p 的单个访问” 。

2
Rationale中的引用似乎表明意图与规范文本不同... :-( - R.. GitHub STOP HELPING ICE
2
即使一个对象是“普通的”,我建议仍然需要考虑中断、其他线程等可能需要使用volatile语义访问它的情况,但要受到非volatile访问可能以意想不到的方式行事的限制。在现实世界中,代码需要在运行时分配缓冲区并使用volatile语义进行访问的情况是存在的。如果从malloc返回的缓冲区被认为是“易失性的”,那么通过非易失性指针进行的访问将调用UB,因此这样的缓冲区必须是“普通的”。但是,如果普通性... - supercat
2
缓冲区意味着编译器可以忽略指向其内容的“volatile”限定符,而在上下文之间共享数据所必需的语义将无法实现。 - supercat
1
@philipxy:如果不使用malloc,代码如何动态分配内存以满足需要具有“volatile”语义的进程? - supercat
1
我希望我能为这个更新再次点赞,因为它包含了DR! - underscore_d
显示剩余2条评论

2
不完全确定,但我认为重点在于对象具有的类型和对象被定义的类型之间的区别。
来自C11(n1570)6.3.2.1 p1(省略脚注,强调我的部分): lvalue是一个表达式(具有非void对象类型),它可能指定一个对象;如果lvalue在求值时没有指定对象,则行为未定义。当说一个对象具有特定类型时,类型由用于指定对象的lvalue指定。[...]
正是lvalue定义了对象在特定访问中具有的类型。相反,*p不表示已定义为volatile的对象。例如,ibid。6.7.3 p6(强调我的部分)读取
[...]如果尝试通过使用非易失性限定类型的lvalue引用定义为易失性限定类型的对象,则行为未定义。133)
133)这适用于那些表现为使用限定类型定义的对象的对象,即使它们实际上在程序中从未定义为对象(例如,内存映射输入/输出地址处的对象)。
如果旨在允许优化掉所示代码,则问题中的引用可能会读取对象has [已定义为]易失性限定类型可能被修改[...]。
标识符的“定义”*)在6.7第5段中定义。
同样,ibid。 6.7.3 p7(一个具有易失性限定类型的对象的访问构成是实现定义的。)给予了实现者一些自由裁量权,但在我看来,修改由n表示的对象的副作用应该被符合的实现视为可观察到。
*)据我所知,标准没有在任何地方定义“已定义为(某种类型的)对象”,因此我将其解读为“由定义声明的标识符指定的对象(某些类型)”。

基于您对“has”一词的分析,我倾向于接受这种解释。在“modifiable lvalue”的定义中,与const相似的语言出现在“不具有const限定类型”的定义中。在那种情况下,如果“have”指的是对象在其定义中声明的类型而不是正在使用的表达式的类型,则*(const int *)&x将是可修改的lvalue,这显然不是意图。 “拥有对象”的含义与“可能指定没有对象的表达式”的含义之间存在一些差异。 - R.. GitHub STOP HELPING ICE
但是对我来说,原始引用文本中的“对象”似乎仍然合理,即由左值指定的对象,具有左值表达式的类型。 - R.. GitHub STOP HELPING ICE
我会对“可比较语言”分析持谨慎态度;该标准是由许多人通过许多编辑制定的,因此他们不太可能为这两个地方同步语言而付出努力。 - M.M

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接