允许编译器对局部volatile进行常量折叠吗?


25

考虑以下简单代码:

void g();

void foo()
{
    volatile bool x = false;
    if (x)
        g();
}

https://godbolt.org/z/I2kBY7

您可以看到,也gcc没有clang优化对的潜在调用g。在我的理解中,这是正确的:抽象机器假定volatile变量随时可能更改(由于例如被硬件映射),因此将false初始化不断折叠到if检查中将是错误的。

但是MSVC g完全消除了对的调用(保留对文件的读写volatile!)。这是符合标准的行为吗?


背景:我有时会使用这种结构来即时打开/关闭调试输出:编译器必须始终从内存中读取值,因此在调试过程中更改变量/内存应相应地修改控制流。 。MSVC输出确实重新读取了该值,但忽略了该值(可能是由于不断折叠和/或消除了死代码),这当然违背了我的意图。


编辑:

  • volatile此处讨论了对读写的消除:是否允许编译器优化局部volatile变量?(感谢内森!)。我认为该标准非常明确,即必须进行那些读写操作。但是,该讨论并未涵盖编译器将这些读取结果视为理所当然并基于此进行优化是否合法。我想这是标准中未指定/未指定的,但是如果有人证明我做错了,我会很高兴。

  • 我当然可以制作x一个非局部变量来避免该问题。这个问题更多是出于好奇。


3
在我看来,这似乎是一个显而易见的编译器错误。
Sam Varshavchik

1
据我所知,这似乎是合法的。编译器可以证明,即使对象是易失性的,也无法对其状态进行修改以便将其折叠起来。我没有足够的信心回答这个问题,但我认为这是正确的。
NathanOliver

3
但是我认为OP关于变量可以由调试器修改的观点是合理的。也许有人应该向MSVC提交错误报告。
布莱恩

2
@curiousguy即使您放弃结果和/或采用确切的值,您仍然可以阅读它。
Deduplicator

2
有趣的是,它仅适用于x64。x86版本仍然调用g()godbolt.org/z/nc3Y-f
Jerry Jeremiah

Answers:


2

我认为[intro.execution](段号不同)可以用来解释MSVC行为:

具有自动存储持续时间的每个对象的实例与其块中的每个条目相关联。这样的对象存在,并在块执行期间和块挂起时保留其最后存储的值

该标准不允许消除易读的glvalue的读取,但以上段落可以解释为允许预测该值false


顺便说一句,C标准(N1570 6.2.4 / 2)指出

一个对象存在,具有恒定的地址,并在其生命周期内保留其最后存储的值。34


34)对于易失性对象,最后一个存储不必在程序中是明确的。

尚不清楚在C内存/对象模型中是否可以自动存储持续时间在对象中进行非显式存储。


同意编译器可能知道目标平台上何时可能出现非显式存储
MM

1
因此,如果这是真的,那么本地易失性对象(至少在MSVC上)完全没有意义吗?volatile如果出于优化目的而忽略了添加,那么有什么可以买到的(除了多余的读取/写入)?
马克斯·朗霍夫

1
@MaxLanghof在完全没有意义和没有想要/期望的效果之间有区别。
Deduplicator

1
@MaxLanghof浮点计算的中间结果有时会提升为80位寄存器,从而导致精度问题。我相信这是一种gcc主义,可以通过将所有此类double声明为volatile来避免。请参阅:gcc.gnu.org/bugzilla/show_bug.cgi?
id=323

1
@philipxy PS请参阅我的答案,我知道访问权限是实现定义的。问题不是关于访问(访问对象),而是关于值的预测。
语言律师

2

TL; DR编译器可以对每个易失性访问执行所需的任何操作。但是文档必须告诉您。--“通过易失性glvalue进行访问的语义是实现定义的。”


该标准为程序定义了实现必须遵循“假设条件”的“易失性访问”和其他“可观察到的行为”(通过“副作用”实现)的允许序列。

但是标准说(我的黑体字强调):

编程语言C ++标准工作草案
文档编号:N4659日期
:2017-03-21

§10.1.7.1 cv限定词

5 通过易失性glvalue进行访问的语义是实现定义的。[…]

对于交互式设备也是如此(我的黑体字强调):

§4.6程序执行

5执行格式正确的程序的一致实现应产生与具有相同程序和相同输入的抽象机相应实例的可能执行之一相同的可观察行为。[...]

7符合标准的实施的最低要求是:

(7.1)-严格根据抽象机的规则评估通过易失性glvalue进行的访问。
(7.2)—在程序终止时,写入文件的所有数据应与根据抽象语义执行程序可能产生的结果之一相同。
(7.3)—交互式设备的输入和输出动态应该以一种方式进行,即在程序等待输入之前,实际上会发出提示输出。构成交互式设备的是实现定义的。

这些统称为程序的可观察行为。[...]

(标准未指定为程序生成的特定代码。)

因此,尽管该标准说不能从某些机器代码(也许)定义的抽象机器副作用和随之而来的可观察行为的抽象序列中消除易失性访问,但是您不能期望任何东西都会反映在目标代码或现实世界中除非您的编译文档告诉您什么构成易失访问。交互式设备的同上。

如果你有兴趣在挥发性相一相的抽象机副作用和/或由此产生的观察到的行为抽象序列,一些代码(也许)定义,然后这么说。但是,如果您对生成相应的目标代码感兴趣,则必须在编译器和编译上下文中对其进行解释。

长期以来,人们错误地认为,对于易失性访问,抽象机评估/读取会导致实现读取,而抽象机分配/写入会导致实现写入。没有实现文档的这种信念是没有根据的。当/ iff实现说它实际上在“易失性访问” 上做某事时,人们有理由期望做某事-也许是某些目标代码的生成。


1
我的意思是,这可以归结为“如果我想出一台上述所有副作用都是无操作的机器,那么我就可以通过将每个程序编译为无操作来实现合法的C ++实现”。当然,我们对实际上可观察到的效果感兴趣,因为抽象的机器副作用是重言式抽象的。这确实需要一些副作用的基本概念,并且我会认为“易失性访问导致显式的内存访问指令”是其中的一部分(即使标准无关紧要),所以我并不真的购买“说”如果您想要代码而不是抽象语义”。不过,+ 1。
马克斯·朗霍夫

是的,执行质量很重要。但是,除了写入文件之外,“可观察对象” [sic]是实现定义的。如果您希望能够设置断点,访问某些实际内存,在抽象易失性读写操作中忽略“易失性”等,则必须让编译器编写器输出适当的代码。PS在C ++中,“副作用”本身并不用于观察行为,而是用于描述子表达式的部分顺序评估。
philipxy

关心解释[原文如此]?您引用的是哪个来源,这是什么错误?
Max Langhof

简而言之,我只是说一个抽象的机器可观察到的东西只有在现实世界中是可以观察到的,以及实现的方式。
philipxy

1
您是在说一种实现可以声称没有交互式设备之类的东西,所以任何程序都可以做任何事情,并且仍然是正确的吗?(注意:我不理解您对交互式设备的重视。)
curiousguy

-1

我认为跳过支票是合法的。

每个人都喜欢引用的段落

34)对于易失性对象,最后一个存储不必在程序中是显式的

并不意味着实现必须假定此类存储在任何时间或任何易变变量中都是可能的。一个实现知道哪些存储是可能的。例如,完全合理的假设是,此类隐式写入仅发生在映射到设备寄存器的易失性变量上,并且这种映射仅适用于具有外部链接的变量。或者一种实现方式可以假定这种写操作只对字大小,字对齐的存储器位置进行了写操作。

话虽如此,我认为MSVC行为是一个错误。没有现实世界的理由可以优化通话。这样的优化可能是合规的,但这是不必要的。


你能解释为什么它是邪恶的吗?在代码显示中,该函数实际上从不被调用。
David Schwartz

@DavidSchwartz您只能得出结论,在指定局部易变变量的语义之后(请参见上面引用的段落)。该标准本身指出,这volatile应该是对实现的提示,即该值可以通过实现未知的方式进行更改。
Max Langhof

@MaxLanghof无法实现无法正确处理它未知的内容。有用的平台实际上所做的就是指定volatile在该平台上以及该规范之外您可以使用或不能使用的功能,这始终是胡扯。
大卫·史瓦兹

@DavidSchwartz当然可以-通过遵循抽象机的语义(尤其是读取和写入)。它可能无法正确优化-这就是标准的重点。现在,这只是一个注释,因此不是规范性的,而且正如我们都说的那样,实现可以指定要做什么volatile并坚持执行。我的观点是,代码本身(根据C ++标准/抽象机)不允许您确定是否g可以调用。
Max Langhof

@DavidSchwartz该代码没有显示类似的内容,因为没有隐式写不会从代码中消失。
n。代词
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.