Let us start from a common mistake made by programmers learning simple data structures in C. The following code is an implementation for simple linked lists of integers in C.
Liquid error: undefined method `join’ for #<String:0x007fad8ab74078>
The idea is simple: we insert
2 into the queue and expect the output to be
42, the digits being output in the order of their insertion into the queue.
However, running the program, we have a big surprise:
$ gcc -Wall -Wextra 1.c -g -o 1-gcc $ clang -Wall -Wextra 1.c -g -o 1-clang $ ./1-gcc 24 $ ./1-clang 42
Let’s ignore for now the fact that clang gives the correct response and let’s answer this question: What went wrong? The queue implementation seems correct but let’s replace it by a dummy implementation:
Liquid error: undefined method `join’ for #<String:0x007fad8ab6faa0>
Basically, we have removed the queue and replaced it by it’s length, stored in the global
x variable. The expected output is
$ clang -Wall -Wextra 2.c -g -o 2-clang $ gcc -Wall -Wextra 2.c -g -o 2-gcc $ ./2-gcc 01 $ ./2-clang 10
The output is consistent with the above example. Thus, the problem is not in our queue implementation. It must be somewhere else.
Before starting to shout that
clang is buggy, let us read the C standard:
unspecified behavior: use of an unspecified value, or other behavior where this International Standard provides two or more possibilities and imposes no further requirements on which is chosen in any instance
EXAMPLE An example of unspecified behavior is the order in which the arguments to a function are evaluated.
This explains why the two compilers were allowed to give different results, while being both correct.
If we want more informations, we can compare the generated assembly code of the two compilers. The
gcc version is below (only the relevant snippet):
call inc movl $0, %eax call inc movl $0, %eax call dec movl %eax, %ebx movl $0, %eax call dec movl %eax, %ecx movl $.LC0, %eax movl %ebx, %edx movl %ecx, %esi movq %rax, %rdi movl $0, %eax call printf
For comparation, the
clang version is:
callq inc movl %eax, -8(%rbp) # 4-byte Spill callq inc movl %eax, -12(%rbp) # 4-byte Spill callq dec movl %eax, -16(%rbp) # 4-byte Spill callq dec leaq .L.str, %rdi movl -16(%rbp), %esi # 4-byte Reload movl %eax, %edx movb $0, %al callq printf
The understanding of the assembly code is left as an exercise to the reader. It is easy to see that
clang uses the stack to store the results.
Both snippets were obtained using no optimization. As an exercise, try to observe the effect of each optimization level on the generated code and explain it.
Let’s go on. In the above snippet we have used functions to increment and decrement the global value. Let us rewrite that code:
Liquid error: undefined method `join’ for #<String:0x007fad8ab6f6b8>
Here, we have another suprise:
$ ./3-gcc 00 $ ./3-clang 10
gcc version printed only the final value of
x, twice. Clearly, there is something happening here. Let’s look closer.
Looking at the generated assembly, we see that
gcc generated this code:
movl $0, -4(%rbp) addl $1, -4(%rbp) addl $1, -4(%rbp) subl $1, -4(%rbp) subl $1, -4(%rbp) movl $.LC0, %eax movl -4(%rbp), %edx movl -4(%rbp), %ecx movl %ecx, %esi movq %rax, %rdi movl $0, %eax call printf
On the other hand,
movl -8(%rbp), %eax addl $1, %eax movl %eax, -8(%rbp) movl -8(%rbp), %eax addl $1, %eax movl %eax, -8(%rbp) movl -8(%rbp), %eax addl $4294967295, %eax # imm = 0xFFFFFFFF movl %eax, -8(%rbp) movl -8(%rbp), %ecx addl $4294967295, %ecx # imm = 0xFFFFFFFF movl %ecx, -8(%rbp) movl %eax, %esi movl %ecx, %edx movb $0, %al callq printf
One can easily see that
gcc did the operations on
x before starting the function call sequence while
clang interleaved stack operations with operations on
x such that the output is the one we would expect. Not to mention the fact that substraction was replaced by addition.
However, what allows the compilers to do this? Luckily, we have compiled with warnings on:
$ clang -Wall -Wextra 3.c -g -o 3-clang $ gcc -Wall -Wextra 3.c -g -o 3-gcc 3.c: In function ‘main’: 3.c:9:24: warning: operation on ‘x’ may be undefined [-Wsequence-point]
Reading the standard for sequence points we get:
Evaluation of an expression may produce side effects. At certain specified points in the execution sequence called sequence points, all side effects of previous evaluations shall be complete and no side effects of subsequent evaluations shall have taken place.
Reading further, we see that it is an undefined behaviour if:
Between two sequence points, an object is modified more than once, or is modified and the prior value is read other than to determine the value to be stored.
Lastly, the standard defines a sequence point to be (among other more complex constructs):
- a call of a function
- end of the first operand or
Thus, our side effects in the
printf function cause undefined behaviour and unspecified behaviour. Depending on the compiler, the results can be very different. This harms portability and should be avoided.
Before finishing the article, let’s see another example, using different constructs:
Liquid error: undefined method `join’ for #<String:0x007fad8ab6f078>
The possible outputs of this code and the reasoning behind are left as an exercise. Use the comments area to provide solutions for all exercises left in this article.
As a rule of thumb, try to limit the use of side effects inside a function call. You don’t know when you’ll fall into this trap again.