1
votes

From C standard ISO/IEC 9899:201x 5.1.2.3p6:

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 behavior of the program.

The meaning of this paragraph is quite dramatic (at least for me), the way I see it, this paragraph says that:

(1) A compiler which produce the same observable behavior, as would a fully-standard-conforming abstract machine produce, is a conforming compiler, which means that all of the other requirements and paragraphs in the standard are just extra (beside sections of volatiles and 7.21.3), for example a conforming compiler can actually break the rules of order-of-evaluations (a && b) as long as the observable behaviour (volatiles, files content and interactive output) is correct.

(2) A program that does not have volatiles, does not write to files and has no input-output interactive, is a program which actually does nothing, has no observable behaviour and can be fully optimized to, for example, two instruction of xor eax, eax than ret (x86-64 clang 7.0.0) in the main.

Am I right, or way off about this?

2
From 5.1.2.3/1: "The semantic descriptions in this International Standard describe the behavior of an abstract machine in which issues of optimization are irrelevant." To be conformant the "semantic descriptions" of the "abstract machine" must be followed. And the "semantic descriptions" of that "abstract machine" is just about all the rest of the specification. - Some programmer dude
I don't see what is "dramatic" here. Any particular objections? - Kit.
Am I right about my second point? - user2162550
Yes, you are right. It can, unless there are other implementation-defined guarantees. - Kit.
@P__J__ : not exactly. The program produced by a conforming implementation shall behave as if the static storage objects have been initialized. If it can know that they are no used, it can optimize them out. - Serge Ballesta

2 Answers

2
votes
  1. Yes, a C implementation (not just a compiler—the standard library, linking, and run-time support, and/or whatever else is used to implement C, are included), which produces the same observable behavior1 as a conforming abstract machine is conforming. All the other requirements and paragraphs in the standard are not just extra. They define the behavior of the abstract machine, so they contribute to describing what the observable behavior must be.

  2. Yes, a program that has no observable behavior may be optimized to a program that merely returns. Note that the standard does not actually include exit status in observable behavior, so the xor eax, eax is technically not required. However, this is likely just an inadvertent defect in the standard, not the intent.

Notes

1 The behavior of programs is not the only thing required by the specification. Implementations must also document various implementation-defined behavior, for example. So this hypothetical C implementation that behaves identically to some abstract machine must also include the required documentation.

2
votes

Yes you are right. A compiler can do what it wants, provided the observable behaviour is the same a what an abstract machine could produce. But this is not dramatic per se: why would we care about something that cannot be observed? This is the point of optimizing compilers.

Example:

int main() {
    int a;
    for (int i=INT_MAX; i>=0; i--) {
        a = i;
    }
    printf("%d\n", a);
    return 0;
}

The only observable behaviour it that it will print 0 at a time. So the compiler can optimize away the loop to produce that same as:

int main() {
    printf("%d\n", 0);
    return 0;
}

What it means it essentially that you cannot use empty loops to add delays, because they could be optimized out producing no delay at all.

IMHO the most dramatic side effect, if that the compiler is allowed to assume that no undefined behaviour can occur in a program.

Second example:

int main() {
    struct {
        int a[16];
        int b[16];
    } s;

    for (i=0; i<16; i++) {
        s.a[i] = i;
        s.b[i] = 2 * i;
    }
    for (i=0; i<32; i++) {
        printf(" %d", s.a[i]);      // UB array access past upper bound
    }
    printf("\n");
    return 0;
}

A naive compiler should display all numbers from 0 to 31, because we know that arrays s.a and s.b should be adjacent and pointer arithmetics should give &(s.b[0]) == &(s.a[16]). But an optimizing compiler can note that s.b values are never used in the observable behaviour if no UB is involved and is free to optimize away the s.b array accesses, and even optimize out the b member. Crash or random values are to be expected here... Worse, a really clever compiler could notice that there are past bound accesses in the printing loop. From that on the program behaviour is undefined, and the compiler could for example stop the loop after printing the 16-st value. No error but only 16 values printed...