clanguage-lawyerc99volatilec11

What does section 5.1.2.3, paragraph 4 (in n1570.pdf) mean for null operations?


I have been advised many times that accesses to volatile objects can't be optimised away, however it seems to me as though this section, present in the C89, C99 and C11 standards advises otherwise:

... An actual implementation need not evaluate part of an expression if it can deduce that its value is not used and that no needed side effects are produced (including any caused by calling a function or accessing a volatile object).

If I understand correctly, this sentence is stating that an actual implementation can optimise away part of an expression, providing these two requirements are met:

It seems to me that many people are confusing the meaning for "including" with the meaning for "excluding".

Is it possible for a compiler to distinguish between a side effect that's "needed", and a side effect that isn't? If timing is considered a needed side effect, then why are compilers allowed to optimise away null operations like do_nothing(); or int unused_variable = 0;?

If a compiler is able to deduce that a function does nothing (eg. void do_nothing() { }), then is it possible that the compiler might have justification to optimise calls to that function away?

If a compiler is able to deduce that a volatile object isn't mapped to anything crucial (i.e. perhaps it's mapped to /dev/null to form a null operation), then is it possible that the compiler might also have justification to optimise that non-crucial side-effect away?

If a compiler can perform optimisations to eliminate unnecessary code such as calls to do_nothing() in a process called "dead code elimination" (which is quite the common practice), then why can't the compiler also eliminate volatile writes to a null device?

As I understand, either the compiler can optimise away calls to functions or volatile accesses or the compiler can't optimise away either, because of 5.1.2.3p4.


Solution

  • I think the "including any" applies to the "needed side-effects" , whereas you seem to be reading it as applying to "part of an expression".

    So the intent was to say:

    ... An actual implementation need not evaluate part of an expression if it can deduce that its value is not used and that no needed side effects are produced .

    Examples of needed side-effects include:

    • Needed side-effects caused by a function which this expression calls
    • Accesses to volatile variables

    Now, the term needed side-effect is not defined by the Standard. Section /4 is not attempting to define it either -- it's trying (and not succeeding very well) to provide examples.

    I think the only sensible interpretation is to treat it as meaning observable behaviour which is defined by 5.1.2.3/6. So it would have been a lot simpler to write:

    An actual implementation need not evaluate part of an expression if it can deduce that its value is not used and that no observable behaviour would be caused.


    Your questions in the edit are answered by 5.1.2.3/6, sometimes known as the as-if rule, which I'll quote here:

    The least requirements on a conforming implementation are:

    • Accesses to volatile objects are evaluated strictly according to the rules of the abstract machine.
    • At program termination, all data written into files shall be identical to the result that execution of the program according to the abstract semantics would have produced.
    • The input and output dynamics of interactive devices shall take place as specified in 7.21.3. The intent of these requirements is that unbuffered or line-buffered output appear as soon as possible, to ensure that prompting messages actually appear prior to a program waiting for input.

    This is the observable behaviour of the program.

    Answering the specific questions in the edit:

    Is it possible for a compiler to distinguish between a side effect that's "needed", and a side effect that isn't? If timing is considered a needed side effect, then why are compilers allowed to optimise away null operations like do_nothing(); or int unused_variable = 0;?

    Timing isn't a side-effect. A "needed" side-effect presumably here means one that causes observable behaviour.

    If a compiler is able to deduce that a function does nothing (eg. void do_nothing() { }), then is it possible that the compiler might have justification to optimise calls to that function away?

    Yes, these can be optimized out because they do not cause observable behaviour.

    If a compiler is able to deduce that a volatile object isn't mapped to anything crucial (i.e. perhaps it's mapped to /dev/null to form a null operation), then is it possible that the compiler might also have justification to optimise that non-crucial side-effect away?

    No, because accesses to volatile objects are defined as observable behaviour.

    If a compiler can perform optimisations to eliminate unnecessary code such as calls to do_nothing() in a process called "dead code elimination" (which is quite the common practice), then why can't the compiler also eliminate volatile writes to a null device?

    Because volatile accesses are defined as observable behaviour and empty functions aren't.