Making oughts from ises
Pascal Cuoq - 13th Apr 2013A previous post discussed the nature of uninitialized and indeterminate memory throughout the C standards. The argument was “avoid using uninitialized data even if you think you know what you are doing; you may be right but regardless your compiler might think it knows what you are doing and be wrong about it”. I tried to strengthen this argument by claiming that writing the example for that post was how I found my first compiler bug “by hand”.
An example involving unsequenced addition and sequenced comma operators
That bit about a compiler bug being my first I now fear was an exaggeration. I had found and forgotten another arguable GCC bug in 2012 while investigating sequence points. First consider the program below.
#include <stdio.h> int i j; int f(void) { i++; return j; } int g(void) { j++; return i; } int main() { printf("%d" f() + g()); }
Although functions f()
and g()
may be called in an unspecified order calling a function and returning from it are sequence points. The program therefore avoids the sin of undefined behavior caused by C99's clause 6.5:2.
6.5:2 Between the previous and next sequence point an object shall have its stored value modified at most once by the evaluation of an expression. Furthermore the prior value shall be read only to determine the value to be stored.
The program can only print an unspecified choice of either f() + g()
when calling f()
first or f() + g()
when calling g()
first the two of which happen to result in the same value 1
.
The same reasoning that makes the C program above always print 1
should make the program below always print 1
too:
$ cat t2.c #include <stdio.h> int i j; int main() { printf("%d" (0 i++ j) + (0 j++ i)); }
But unfortunately:
$ gcc t2.c $ ./a.out 2
This program should print the same result as the previous program (the one with f()
and g()
) because the modifications of j
are sequenced by 0
on the left and i
on the right and similarly for i
. In C99 at least the compiler must pick a choice between adding an incremented i
to the initial value of j
or the other way round. This example should print 1
in short for the same reason that the first example in this post could be expected to print 1
or that an example that prints cos(x) * cos(x) + sin(x) * sin(x)
with MT-unsafe functions cos()
and sin()
can still be expected to print a floating-point value close to 1.0
.
Pet peeve: guessing at what the standard ought to say from what compilers do
This anecdote slowly but surely brings us to the annoyance I intended to denounce all along and this annoyance is when programmers infer what the standard ought to be saying from what compilers do. Any discussion of the kind of corner cases I described recently invite arguments about how compilers want to optimize the generated code as if uninitialized data could not be used or as if the two operands of +
were evaluated in parallel.
This is backwards. Yes compilers want to optimize. But compilers are supposed to follow the standard not the other way round. And section 6 of the C99 standard does not once use the word “parallel”. It says that the evaluation order of sub-expressions is unspecified (6.5:3) and that unsequenced side-effects cause undefined behavior (6.5:2) but nothing about parallel evaluation.
Never mind the example
Actually C99's 6.5:3 clause really says “[…] the order of evaluation of subexpressions and the order in which side effects take place are both unspecified”. I am unsure about the implications of this thing about the order of side-effects. I might even and the long-time reader of this blog will savor this uncharacteristic admission be wrong: perhaps a compiler is allowed to generate code that prints 2 for t2.c after all.
Nevertheless this does not detract from the point I intended to make which is that it is bad engineering practice to take the current behavior of popular compilers as language definition. If the C99 language allows to “miscompile” (0 i++ j) + (0 j++ i)
so be it. But having read the relevant parts of the standard (6.5.2.2:10 6.5.2.2:12 and 5.1.2.3:2 in addition to the previously mentioned clauses) it seems to me that if the standard allows to miscompile the above it also allows to miscompile f(1) + f(2)
for any function f()
with global side-effects.
Global side-effects include temporarily modifying the FPU rounding mode using a statically allocated scratch buffer initializing a table of constants at first call calling
malloc()
andfree()
or evenrandom()
. All these examples are intended to be invisible to the caller and lots of library functions you routinely use may be doing them.
So in this case my argument remains very much the same: that compilers are not implementing aggressive optimizations (yet) in presence of f(1) + f(2)
should be no excuse for not clarifying whether the standard allows them.