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.

Max Langhof
  • 23,383
  • 5
  • 39
  • 72
  • 3
    This looks like an obvious compiler bug to me. – Sam Varshavchik Oct 16 '19 at 12:08
  • @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. – Max Langhof Oct 16 '19 at 12:16
  • Can this have anything to do with P1152 "Deprecating volatile" ? – marcinj Oct 16 '19 at 12:27
  • 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. – Sam Varshavchik Oct 16 '19 at 12:32
  • 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. – C.M. Oct 16 '19 at 12:44
  • Note how compiler doesn't remove read `x` even though result isn't used and it believes it knows the result of that read... :) – C.M. Oct 16 '19 at 12:47
  • @C.M. - It doesn't have to be voaltile at global scope for that to happen, just needs to have external linkage. – StoryTeller - Unslander Monica Oct 16 '19 at 12:54
  • 1
    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. – NathanOliver Oct 16 '19 at 12:55
  • @NathanOliver 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. – Max Langhof Oct 16 '19 at 13:03
  • 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. – NathanOliver Oct 16 '19 at 13:04
  • 1
    @NathanOliver 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_. – Max Langhof Oct 16 '19 at 13:05
  • @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](https://godbolt.org/z/XNeZTy). Even though in latter case optimizer obviously failed. – C.M. Oct 16 '19 at 13:20
  • @NathanOliver: on the other hand, standard says: "Accesses through volatile glvalues are evaluated strictly according to the rules of the abstract machine." – geza Oct 16 '19 at 18:13
  • 1
    @geza 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. – Brian Bi Oct 16 '19 at 18:36
  • 3
    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. – Brian Bi Oct 16 '19 at 18:37
  • 1
    Yes. This exact case was actually given as an example of how `volatile` can be optimized on the reflectors a couple months back. – T.C. Oct 17 '19 at 22:15
  • @T.C. If you have that on good terms please post it as an answer! – Max Langhof Oct 18 '19 at 06:59
  • @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... duh – curiousguy Oct 19 '19 at 02:45
  • @C.M. 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". – curiousguy Oct 19 '19 at 02:51
  • 2
    @curiousguy Even if you discard the result and/or assume the exact value, you have still read it. – Deduplicator Oct 20 '19 at 23:05
  • 2
    Interestingly it only does that for x64. The x86 version still calls g() https://godbolt.org/z/nc3Y-f – Jerry Jeremiah Oct 20 '19 at 23:16
  • @Deduplicator 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". – curiousguy Oct 21 '19 at 01:56
  • 1
    @Deduplicator 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. – curiousguy Oct 21 '19 at 02:07
  • @curiousguy The standard explicitly says that other than "data written into files", "observable" "side-effects" are "implementation-defined". See my answer. – philipxy Oct 23 '19 at 08:09
  • 1
    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`](https://learn.microsoft.com/en-us/cpp/build/reference/volatile-volatile-keyword-interpretation?view=vs-2019). – Cody Gray - on strike Oct 26 '19 at 07:00
  • @CodyGray Very interesting, thanks for the info! In this case there appears to be no difference between `/volatile:iso` and `/volatile:ms` though. – Max Langhof Oct 28 '19 at 08:41

3 Answers3

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.

Language Lawyer
  • 3,378
  • 1
  • 12
  • 29
  • Agree that the compiler may know when non-explicit stores are possible on the target platform – M.M Oct 20 '19 at 22:56
  • 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. – Deduplicator Oct 20 '19 at 23:03
  • @Deduplicator 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. – Language Lawyer Oct 21 '19 at 01:23
  • That's a constraint on implementations, not a constraint on memory content. – curiousguy Oct 21 '19 at 01:57
  • 1
    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? – Max Langhof Oct 21 '19 at 07:50
  • 1
    @MaxLanghof There's a difference between entirely pointless and not having quite the effect you want/expect. – Deduplicator Oct 21 '19 at 11:06
  • @Deduplicator 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. – Max Langhof Oct 21 '19 at 11:29
  • @MaxLanghof Also, if it is a device-register, reading might have an effect. – Deduplicator Oct 21 '19 at 14:07
  • 1
    @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: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=323 – ForeverLearning Oct 22 '19 at 15:28
  • "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. – philipxy Oct 23 '19 at 08:47
  • 1
    @philipxy _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. – Language Lawyer Oct 24 '19 at 08:04
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.

philipxy
  • 14,867
  • 6
  • 39
  • 83
  • 1
    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. – Max Langhof Oct 23 '19 at 08:41
  • 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. – philipxy Oct 23 '19 at 08:57
  • Care to explain the [sic]? Which source are you quoting and what's the mistake? – Max Langhof Oct 23 '19 at 09:45
  • Re sic I just mean that an abstract machine observable is only real-world observable if & how the implementation says it is. – philipxy Oct 23 '19 at 10:02
  • 1
    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.) – curiousguy Oct 24 '19 at 05:09
  • 1
    @curiousguy 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*. – philipxy Oct 24 '19 at 08:42
  • @curiousguy 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. – philipxy Oct 24 '19 at 08:42
  • "_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. – curiousguy Oct 26 '19 at 03:55
  • @curiousguy 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. – philipxy Oct 26 '19 at 07:00
  • That's clearly specified: "Accesses through volatile glvalues are evaluated **strictly according to the rules of the abstract machine**" – curiousguy Oct 26 '19 at 07:58
-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.

n. m. could be an AI
  • 112,515
  • 14
  • 128
  • 243
  • Can you explain why it's evil? In the code shows, the function can literally never be called. – David Schwartz Oct 24 '19 at 07:36
  • @DavidSchwartz You can only conclude that after you specify the semantics of local volatile variables (see the quoted paragraph above). The [standard itself](http://eel.is/c++draft/dcl.type.cv#6.sentence-1) notes that `volatile` is supposed to be a hint to the implementation that the value can change by means unknown to the implementation. – Max Langhof Oct 24 '19 at 07:45
  • @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. – David Schwartz Oct 24 '19 at 07:57
  • @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. – Max Langhof Oct 24 '19 at 08:07
  • @DavidSchwartz The code does not show anything like that, because absence of implicit writes does not follow from the code. – n. m. could be an AI Oct 24 '19 at 08:56
  • /s/bug/quality of implementation issue (It's not technically a bug if it's standards-compliant.) – Cody Gray - on strike Oct 26 '19 at 07:03
  • @CodyGray It's a bug if it doesn't do what it's intended to do. Otherwise it's a feature. – n. m. could be an AI Oct 26 '19 at 09:16