Search code examples
clanguage-lawyervolatilesequence-points

Formal understanding of volatile semantic


5.1.2.3 defines the following:

In the abstract machine, all expressions are evaluated as specified by the semantics. 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 through volatile access to an object).

The definition sounds a bit vague to me. I'm particularly concerned about Sequenced before definition and how (if) it is tied to volatile.

Consider the following function as an example:

void foo(volatile int* a, int *b, volatile int *c){
    *a = 1;
    *b = 2;
    *c = 3;
}

Basically I have 2 questions:

  1. Is *a = 1; sequenced before *c = 3?
  2. Is *a = 1; sequenced before *b = 2;?

If so, why?

Sequenced before is defined at 5.1.2.3 as:

Sequenced before is an asymmetric, transitive, pair-wise relation between evaluations executed by a single thread, which induces a partial order among those evaluations


Solution

  • Further down in the same part of the "sequenced before" definition C17 5.1.2.3 §3:

    The presence of a sequence point between the evaluation of expressions A and B implies that every value computation and side effect associated with A is sequenced before every value computation and side effect associated with B.

    This is crystal clear and leaves no room for interpretations.

    In your code, every ; marks a sequence point (end of a full expression). So it is without doubt sequenced from the top to the bottom and that much has nothing to do with volatile.


    Another question is if the optimizing compiler is allowed to re-order the expressions. The C concept of "the abstract machine" is muddy and generally unhelpful, but what it says is:

    In the abstract machine, all expressions are evaluated as specified by the semantics. 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).
    /--/
    Accesses to volatile objects are evaluated strictly according to the rules of the abstract machine.

    "The semantics" and "the rules of the abstract machine" refers (among other things) to the "sequenced before" part previously mentioned.

    This can't very well get interpreted in any other way than that instruction re-ordering of volatile object access is forbidden.

    However, the *b=2; expression is not volatile qualified. Here's where it gets muddy. One may read the quoted parts above as "no instruction re-ordering across volatile access is allowed". That is - volatile access must act as a memory barrier - which I think is the correct interpretation.

    But as it happens, various CPU manufacturers implementing concurrent execution with pre-fetch and/or pipelining in multiple cores aren't necessarily digging this deep in the C standards during design. So the hardware might dictate how re-ordering will happen and then there's just so much the compiler vendor can do to fix it. Such hardware and the compiler for that hardware port might be a non-conforming implementation of the C language.


    We may also note that the behavior of volatile changes slightly in the upcoming C23. A volatile access is now treated just as a volatile object access, meaning that stuff like *(volatile int*)0x1234 didn't strictly speaking play by the same rules as volatile int* ptr = (volatile int*)0x1234; ... *ptr. This was a defect, there is a defect report, and it has been fixed in C23.