Is the compiler allowed to constant-fold a local volatile?
Asked Answered
I

3

25

Consider this simple code:

void g();

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

https://godbolt.org/z/I2kBY7

You can see that neither gcc nor clang optimize out the potential call to g. This is correct in my understanding: The abstract machine is to assume that volatile variables may change at any moment (due to being e.g. hardware-mapped), so constant-folding the false initialization into the if check would be wrong.

But MSVC eliminates the call to g entirely (keeping the reads and writes to the volatile though!). Is this standard-compliant behavior?


Background: I occasionally use this kind of construct to be able to turn on/off debugging output on-the-fly: The compiler has to always read the value from memory, so changing that variable/memory during debugging should modify the control flow accordingly. The MSVC output does re-read the value but ignores it (presumably due to constant folding and/or dead code elimination), which of course defeats my intentions here.


Edits:

  • The elimination of the reads and writes to volatile is discussed here: Is it allowed for a compiler to optimize away a local volatile variable? (thanks Nathan!). I think the standard is abundantly clear that those reads and writes must happen. But that discussion does not cover whether it is legal for the compiler to take the results of those reads for granted and optimize based on that. I suppose this is under-/unspecified in the standard, but I'd be happy if someone proved me wrong.

  • I can of course make x a non-local variable to side-step the issue. This question is more out of curiosity.

Intricacy answered 16/10, 2019 at 12:1 Comment(26)
This looks like an obvious compiler bug to me.Cretinism
@SamVarshavchik That's my intuition too, but given that the standard specifically talks about volatile reads and writes themselves as observable side effects (which are preserved here!) I'm not 100% convinced. In my mind it boils down to the exact abstract machine semantics of volatile and their consequences (does volatile prevent e.g. escape analysis?) and whether there is some wiggle room in the standard to sanction MSVC's behavior here.Intricacy
Can this have anything to do with P1152 "Deprecating volatile" ?Slunk
A read being an "observable side effect" means that there is no guarantee that you will read back what you just wrote. Historically, volatile was meant to support memory-mapped I/O registers. Read them five times in a row, get five different values.Cretinism
In this case volatile variable is on the stack -- I could see how compiler may assume it is never gets modified... if you move it to global scope -- g() gets used.Histamine
Note how compiler doesn't remove read x even though result isn't used and it believes it knows the result of that read... :)Histamine
@Histamine - It doesn't have to be voaltile at global scope for that to happen, just needs to have external linkage.Oratorio
As far as I know this is legal under the as if rule. The compiler can prove that even though the object is volatile there is no way for it's state to be modified so it can be folded out. I'm not confident enough to put that in an answer, but I feel it is correct.Subcritical
@Subcritical I appreciate the dupe, but it doesn't really answer my specific question. The titles are similar, but in the dupe's case I believe the standard's stance is quite clear (reads and writes from/to volatile are observable side effects, they must stay, see the highest voted answer there), whereas this question is specifically about the effects beyond the writes and reads.Intricacy
How is the question beyond reads and writes? You are asking if the compiler can optimize out the read in the if statement and just use the value you wrote.Subcritical
@Subcritical All compilers still perform the volatile reads and writes in my code, this is both expected and correct. Note that even MSVC loads from the memory address of the local. What is not expected is that the value read is ignored.Intricacy
@StoryTeller ...or anything else that will prevent compiler from proving x value isn't going to change, like passing address of x to unknown function, or taking an address. Even though in latter case optimizer obviously failed.Histamine
@NathanOliver: on the other hand, standard says: "Accesses through volatile glvalues are evaluated strictly according to the rules of the abstract machine."Terylene
@Terylene ok, but logically there are two separate steps: "access" the glvalue by performing the lvalue-to-rvalue conversion, then use the resulting prvalue to determine whether to evaluate g(). So, perhaps optimizing the second part, when you already know what the prvalue is going to be, is legal.Presswork
But I think the OP's argument that the variable may be modified by a debugger is reasonable. Maybe someone should file a bug report with MSVC.Presswork
Yes. This exact case was actually given as an example of how volatile can be optimized on the reflectors a couple months back.Narcotism
@Narcotism If you have that on good terms please post it as an answer!Intricacy
@Brian The whole purpose of volatile is that there is no optimization. I can't believe one would have to explain it, esp. to a compiler writer. You can't expect any precise value from a volatile read (but you can expect a valid value, and valid representation of that value, according the ABI). It's all about the ABI. So unless the ABI says a bool is going to carry a false value... duhConjunction
@Histamine Then it isn't a read. A read results in a result. The result is the value read. If you ignore it, you have a non reading "read".Conjunction
@Conjunction Even if you discard the result and/or assume the exact value, you have still read it.Chump
Interestingly it only does that for x64. The x86 version still calls g() godbolt.org/z/nc3Y-fHinkle
@Chump It isn't the read that was in the source code then. It's a completely different read whose result is ignored. It only matters how many time you do some kind of read for addresses were a read is not just a read, and causes SE. Most uses of volatile are not on that kind of "memory".Conjunction
@Chump At the end of the day, what matters is what we call "observable". You may view assembly and step by step execution as observable. I don't. I view observables as: debugging info that tells me how to put a breakpoint before each volatile operation, and stack description/debug info that allows me to use ptrace to view and change any volatile object, when program/thread is stopped, such that these ptrace actions do the same as a proper C/C++ obj access done at the point the program was stopped.Conjunction
@Conjunction The standard explicitly says that other than "data written into files", "observable" "side-effects" are "implementation-defined". See my answer.Melancholia
For legacy reasons, MSVC has different semantics for the volatile keyword than dictated by the standard. It is important to know exactly which compiler flags you're using to build this. In particular: /volatile.Scurrility
@CodyGray Very interesting, thanks for the info! In this case there appears to be no difference between /volatile:iso and /volatile:ms though.Intricacy
B
2

I think [intro.execution] (paragraph number vary) could be used to explain MSVC behavior:

An instance of each object with automatic storage duration is associated with each entry into its block. Such an object exists and retains its last-stored value during the execution of the block and while the block is suspended...

The standard does not permit elimination of a read through a volatile glvalue, but the paragraph above could be interpreted as allowing to predict the value false.


BTW, the C Standard (N1570 6.2.4/2) says that

An object exists, has a constant address, and retains its last-stored value throughout its lifetime.34


34) In the case of a volatile object, the last store need not be explicit in the program.

It is unclear if there could be a non-explicit store into an object with automatic storage duration in C memory/object model.

Burgoyne answered 20/10, 2019 at 22:28 Comment(11)
Agree that the compiler may know when non-explicit stores are possible on the target platformSolenoid
Hm. The quote from the C standard includes volatile objects at fixed addresses, instead of being restricted to locals, which can thus map to device-registers, or be accessed from other threads or even programs.Chump
@Chump I've skipped the footnote for has constant address. It says that The term ‘‘constant address’’ means that two pointers to the object constructed at possibly different times will compare equal. The address may be different during two different executions of the same program. I.e. this is not about static vs. automatic storage duration.Burgoyne
That's a constraint on implementations, not a constraint on memory content.Conjunction
So if this is true, then local volatile objects are (at least on MSVC) entirely pointless? Is there anything that adding volatile buys you (other than superfluous reads/writes) if it is ignored for optimization purposes?Intricacy
@MaxLanghof There's a difference between entirely pointless and not having quite the effect you want/expect.Chump
@Chump Which is why I was asking "what does it buy you". Thinking about it more, there's the obvious "don't optimize out side-effect-free function calls by assigning their return value to a volatile", which does indeed make it non-pointless.Intricacy
@MaxLanghof Also, if it is a device-register, reading might have an effect.Chump
@MaxLanghof Intermediate results of floating point calculations sometimes get promoted to a 80-bit register causing precision issues. I believe this is a gcc-ism and is avoided by declaring all such doubles as volatile. See: gcc.gnu.org/bugzilla/show_bug.cgi?id=323Staffer
"It is unclear if there could be a non-explicit store into an object with automatic storage duration in C memory/object model." It's clear--the standard does not say that a volatile can't be automatic & declaring volatile states that a "non-explicit store" can happen at any time. PS See my answer.Melancholia
@Melancholia PS See my answer I know that access is implementation-defined. The question is not about access (the object is accessed), but prediction of the value.Burgoyne
M
2

TL;DR The compiler can do whatever it wants on each volatile access. But the documentation has to tell you.--"The semantics of an access through a volatile glvalue are implementation-defined."


The standard defines for a program permitted sequences of "volatile accesses" & other "observable behavior" (achieved via "side-effects") that an implementation must respect per "the 'as-if' rule".

But the standard says (my boldface emphasis):

Working Draft, Standard for Programming Language C++
Document Number: N4659
Date: 2017-03-21

§ 10.1.7.1 The cv-qualifiers

5 The semantics of an access through a volatile glvalue are implementation-defined. […]

Similarly for interactive devices (my boldface emphasis):

§ 4.6 Program execution

5 A conforming implementation executing a well-formed program shall produce the same observable behavior as one of the possible executions of the corresponding instance of the abstract machine with the same program and the same input. [...]

7 The least requirements on a conforming implementation are:

(7.1) — Accesses through volatile glvalues are evaluated strictly according to the rules of the abstract machine.
(7.2) — At program termination, all data written into files shall be identical to one of the possible results that execution of the program according to the abstract semantics would have produced.
(7.3) — The input and output dynamics of interactive devices shall take place in such a fashion that prompting output is actually delivered before a program waits for input. What constitutes an interactive device is implementation-defined.

These collectively are referred to as the observable behavior of the program. [...]

(Anyway what specific code is generated for a program is not specified by the standard.)

So although the standard says that volatile accesses can't be elided from the abstract sequences of abstract machine side effects & consequent observable behaviors that some code (maybe) defines, you can't expect anything to be reflected in object code or real-world behaviour unless your compiler documentation tells you what constitutes a volatile access. Ditto for interactive devices.

If you are interested in volatile vis a vis the abstract sequences of abstract machine side effects and/or consequent observable behaviors that some code (maybe) defines then say so. But if you are interested in what corresponding object code is generated then you must interpret that in the context of your compiler & compilation.

Chronically people wrongly believe that for volatile accesses an abstract machine evaluation/read causes an implemented read & an abstract machine assignment/write causes an implemented write. There is no basis for this belief absent implementation documentation saying so. When/iff the implementation says that it actually does something upon a "volatile access", people are justified in expecting that something--maybe, the generation of certain object code.

Melancholia answered 23/10, 2019 at 7:37 Comment(10)
I mean, this boils down to "if I come up with a machine where all the mentioned side effects are no-ops then I have a legal C++ implementation by compiling every program to a no-op". Of course we are interested in practically observable effects, since abstract machine side effects are tautologically abstract. This does require some basic concept of side effects, and I would reason that "volatile accesses lead to explicit memory access instructions" is part of that (even if the standard doesn't care), so I don't really buy the "say if you want code instead of abstract semantics". Still, +1.Intricacy
Yes, quality of implementation is relevant. Nevertheless, other than writes to files, "observables" [sic] are implementation-defined. If you want to be able to set breakpoints, access certain actual memory, have 'volatile' ignored, etc on abstract volatile reads & writes then you have to get your compiler writer to output appropriate code. PS In C++ "side-effect" is not used for observabke behaviour per se, it is used for describing partial order evaluation of subexpressions.Melancholia
Care to explain the [sic]? Which source are you quoting and what's the mistake?Intricacy
Re sic I just mean that an abstract machine observable is only real-world observable if & how the implementation says it is.Melancholia
Are you saying that an implementation could claim that there is no such thing as an interactive device, so any program can do anything, and it would still be correct? (Note: I don't understand your emphasis on interactive devices.)Conjunction
@Conjunction I don't understand what argument you think you have against the clear statement right there of "implementation-defined". Moreover I don't know what else you think the standard can do, since it doesn't define what object code is or whether there is any or what a system a program is executed on is. "4.6 1 [...] conforming implementations are required to emulate (only) the observable behavior of the abstract machine as explained below"--that's "emulate" & that's "observable behavior" given by a description parameterized by implementation-defined terms.Melancholia
@Conjunction I only highlighted the "interactive device" (7.3) clause because with "volatile access" (7.1) it is the other explicit case of implementation-defined "observable behavior". Moreover, if you look up what constitutes (7.2) "data being written to a file", it is--what else--not standard-specified: "30.10.2.1 POSIX conformance 1 Some behavior is specified by reference to POSIX (30.10.3). How such behavior is actually implemented is unspecified." Reasonableness of what a program actually does when executed is topic "quality of implementation". Buy a compiler that does what you want.Melancholia
"Accesses through volatile glvalues are evaluated strictly according to the rules of the abstract machine" seems clear: everything else can be emulated, but accesses to volatile objects are for real.Conjunction
@Conjunction I suggest you think about the reasons you hold your beliefs. I suggest you carefully read all of 4 General principles but especially 4.1 Implementation compliance & 4.6 Program execution, referencing 3 Terms and definitions, seeking to understand what it is actually saying & not seeking to try to find some misinterpretation consistent with your prior expectations.Melancholia
That's clearly specified: "Accesses through volatile glvalues are evaluated strictly according to the rules of the abstract machine"Conjunction
E
-1

I believe it is legal to skip the check.

The paragraph that everyone likes to quote

34) In the case of a volatile object, the last store need not be explicit in the program

does not imply that an implementation must assume such stores are possible at any time, or for any volatile variable. An implementation knows which stores are possible. For instance, it is entirely reasonable to assume that such implicit writes only happen for volatile variables that are mapped to device registers, and that such mapping is only possible for variables with external linkage. Or an implementation may assume that such writes only hapen to word-sized, word-aligned memory locations.

Having said that, I think MSVC behaviour is a bug. There is no real-world reason to optimise away the call. Such optimisation may be compliant, but it is needlessly evil.

Exposure answered 24/10, 2019 at 7:23 Comment(7)
Can you explain why it's evil? In the code shows, the function can literally never be called.Medallion
@DavidSchwartz You can only conclude that after you specify the semantics of local volatile variables (see the quoted paragraph above). The standard itself notes that volatile is supposed to be a hint to the implementation that the value can change by means unknown to the implementation.Intricacy
@MaxLanghof There's no way the implementation can correctly handle something unknown to it. What useful platforms actually do is specify what you can and can't use volatile for on that platform and outside that specification, it's always going to be a crap shoot.Medallion
@DavidSchwartz Of course it can - by following the semantics (in particular, the reads and writes) of the abstract machine. It might not be able to optimize correctly - that's the point of the standard. Now, it's a note and thus not normative, and as we both said, the implementation can specify what volatile does and stick to that. My point is that the code itself (according to the C++ standard/abstract machine) does not allow you to determine whether g can be called.Intricacy
@DavidSchwartz The code does not show anything like that, because absence of implicit writes does not follow from the code.Exposure
/s/bug/quality of implementation issue (It's not technically a bug if it's standards-compliant.)Scurrility
@CodyGray It's a bug if it doesn't do what it's intended to do. Otherwise it's a feature.Exposure

© 2022 - 2024 — McMap. All rights reserved.