1

I am trying to compile the below C code with -O3 optimization flag by GCC and Clang, and after looking at the generated assembly code, I find neither of these compilers implements the short circuit evaluation which is mentioned in the C standard for the && operator.

You can refer to the below assembly code for further information, the first five lines of code of the foo function would be run sequentially, and it would compare both two operands of the && operators which actually violates the standard. So, any misunderstandings here?

C code:

#include <stdio.h>
#include <stdbool.h>
void foo(int x, int y) {
  bool logical = x && y;
  printf("%d", logical);
}
int main(void) {
  foo(1, 3);
  return 0;
}

Generated assembly code:

foo:                                    # @foo
        test    edi, edi
        setne   al
        test    esi, esi
        setne   cl
        and     cl, al
        movzx   esi, cl
        mov     edi, offset .L.str
        xor     eax, eax
        jmp     printf                          # TAILCALL
main:                                   # @main
        push    rax
        mov     edi, offset .L.str
        mov     esi, 1
        xor     eax, eax
        call    printf
        xor     eax, eax
        pop     rcx
        ret
.L.str:
        .asciz  "%d"
Jason Yu
  • 1,886
  • 16
  • 26
  • 6
    Remember the "[as if rule](https://stackoverflow.com/questions/15718262/what-exactly-is-the-as-if-rule)". The behavior of the compiler's code is not distinguishable in any way from the short-circuit evaluation, since evaluating `y` has no side effects. – Nate Eldredge Dec 02 '21 at 17:23
  • Since evaluating `y` has no side-effects, there is no distinguishable behavior of it being evaluated or not. – Eugene Sh. Dec 02 '21 at 17:33
  • @EugeneSh. `if(p & *p)` does not have side effects – 0___________ Dec 02 '21 at 17:43
  • @0 not sure of how it is related – Eugene Sh. Dec 02 '21 at 17:44
  • 2
    @0 Ok, I guess you mean `if (p && *p)` idiom for checking if `p` is null and then checking the pointee. It does have a side effect, which is a memory access which can fail. The behavior is clearly distinguishable in case `p==NULL` vs `p !=NULL`. Accessing local variable `y` cannot fail. – Eugene Sh. Dec 02 '21 at 17:53

2 Answers2

5

First, your example is faulty.

Given x = 1 and y = 3, evaluating x && y requires evaluating both operands, since the && is only true if both operands are true.

Second, C 2018 5.1.2.3 6 says:

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.

This means the compiler is only responsible for ensuring the above behaviors occur as specified. It is not required to generate assembly language that does not evaluate the right operand of && or || when the result can be deduced from the left operand as long as that evaluation does not alter the observable behaviors. The compiler has no obligation to generate the assembly language you seek, just to ensure the observable behaviors are as specified.

When the standard describes expressions as being evaluated or not, it is describing them in the context of an “abstract machine” (C 2018 5.1.2.3 1), not describing what the generated program must actually do on the assembly language level. The idea is that the C standard describes what a program would generate in an abstract machine, and then the compiler may generate any program that has the same observable behavior as the program in an abstract machine, even if it obtains that result in a completely different way than the program in the abstract machine does.

Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312
  • Actually, it does not require two operands (considering the function) as it will be false if `x` is `0`. For the constants as parameters compiler simple optimizes the function out and there are no checks at all – 0___________ Dec 02 '21 at 17:35
  • There must be a bit more about it in the standard, otherwise `if( p && *p)` behaviour would be implementation defined. – 0___________ Dec 02 '21 at 17:39
  • Eric sorry I have DV-ted by mistake (wanted to UV, clicked wrongly). System does not let me retract this vote. Please edit your answer (any minor change) to unlock the vote. Also asked https://meta.stackoverflow.com/questions/413450/retracting-the-downvote-even-if-the-answer-was-not-edited as I do not see any reason for such restrictions – 0___________ Dec 02 '21 at 18:07
  • 1
    @0___________: As shown in the assembly in the question, the compiler optimizes away the call to `foo` from `main`, but a full implementation for `foo` is generated because it has external linkage. That implementation receives arguments that may not be constants, so the compiler cannot optimize it away. – Eric Postpischil Dec 02 '21 at 18:09
  • @0___________: Edited; there was a stray back-tick that needed to be removed anyway. – Eric Postpischil Dec 02 '21 at 18:09
  • 1
    @0___________: I do not see why you think `p && *p` would be implementation-defined. If `p` is a pointer, the compiler is free to generate assembly code that “evaluates” `*p`, e.g., that executes a load instruction, provided it knows the load will not generate a fault that changes the observable behavior. If there is a possibility that `p` will be a null pointer and a load instruction would generate a fault, then the compiler must test `p` before executing the load instruction. – Eric Postpischil Dec 02 '21 at 18:12
  • In the `foo` function, if `x` is `0`, the second one does not have to be evaluated and only reason why it is, is the requested code efficiency. Without optimizations, the second one is not evaluated if `x` is `0` https://godbolt.org/z/P9ojadW7E – 0___________ Dec 02 '21 at 18:15
3

It is. But the compiler chooses the fastest way as requested.

  1. main - The compiler knows the result compile time and is not calling function foo at all
  2. foo - Branches are quite expensive operations as they require flushing the pipeline, potential cache miss, reading from slow memory etc. So it makes no sense to branch in this case. Your example is simply too trivial.

Consider a bit less trivial example:

bool bar(void);

void foo(int x) {
  bool logical = x && bar();
  printf("%d", logical);
}
int main(void) {
  foo(1);
  return 0;
}
foo:
        test    edi, edi
        jne     .L12
        mov     esi, edi
        xor     eax, eax
        mov     edi, OFFSET FLAT:.LC0
        jmp     printf
.L12:
        sub     rsp, 8
        call    bar
        mov     edi, OFFSET FLAT:.LC0
        add     rsp, 8
        movzx   esi, al
        xor     eax, eax
        jmp     printf
main:
        sub     rsp, 8
        mov     edi, 1
        call    foo
        xor     eax, eax
        add     rsp, 8
        ret

From C standard 6.5.13:

Unlike the bitwise binary & operator, the && operator guarantees left-to-right evaluation; if the second operand is evaluated, there is a sequence point between the evaluations of the first and second operands. If the first operand compares equal to 0, the second operand is not evaluated.

0___________
  • 60,014
  • 4
  • 34
  • 74
  • The reason for branching here is correctness, not efficiency. `bar()` may have side effects, so it must not be called when the C abstract machine wouldn't call it. Using `__attribute__((const))` (https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html) promises that it's a pure function and only reads its args (retval not affected by observable state of the program), which would it legal to call it unconditionally, but still not what GCC or clang *choose* to do. https://godbolt.org/z/96zr11v85 – Peter Cordes Dec 03 '21 at 03:57