0

i have this simple piece of code in c:

#include <stdio.h>

void test() {}

int main()
{
    if (2 < 3) {

        int zz = 10;
    }
    return 0;
}

when i see the assembly output of this code:

test():
  pushq %rbp
  movq %rsp, %rbp
  nop
  popq %rbp
  ret
main:
  pushq %rbp
  movq %rsp, %rbp
  movl $10, -4(%rbp) // space is created for zz on stack
  movl $0, %eax
  popq %rbp
  ret

i got the assembly from here (default options) I can't see where is the instruction for the conditional check?

anekix
  • 2,393
  • 2
  • 30
  • 57
  • 8
    Optimized away. – Sani Huttunen Feb 22 '18 at 11:04
  • 1
    Your `if` statement is always true: the compiler is very likely clever enough to realize it, so not to produce any code for it. – Laurent H. Feb 22 '18 at 11:05
  • a) your comment is wrong, "movl $10, ..." is initializing zz with it's value 10, without "reserving" it or something b) if you turn on optimization to -O3, all your code will do is "return 0;", for anything else is needed – Tommylee2k Feb 22 '18 at 13:00
  • This is entirely meaningless to discuss unless you state which compiler that was used and how the program was compiled. – Lundin Feb 22 '18 at 14:37
  • @Tommylee2k can you elaborate on this more as i want to understand this or even if you mention the keywords that i need to search, it will be more than helpful to me.now it got me thinking how is the variable name `zz` associated to value 10 ( i mean where is this thing stored)? – anekix Feb 22 '18 at 14:46
  • @Lundin the link of the ompiler is in question (its online , it produces assembly output for a given program) – anekix Feb 22 '18 at 14:48
  • godbolt.org apparently supports a tonne of different compilers, so that's not helpful. And you have to show which compiler options that were used when compiling. – Lundin Feb 22 '18 at 14:53
  • @Lundin the default ones, else i would have mentioned. my apologies if my question couldn't convey the intent.i have edited my question to reflect this – anekix Feb 22 '18 at 14:56
  • So your actual question is why the gcc compiler seems to optimize the code even though it is called with no options and no optimizations enabled (equal to `-O0`)? – Lundin Feb 22 '18 at 15:04
  • @Lundin apparently yes, because i didn't know about compiler optimization so i asked it here , before asking i searched for terms like "conditionals assembly output" in google which was not yielding any relevant result ( because i realize now i was searching the wrong thing) – anekix Feb 22 '18 at 15:06
  • @Lundin aslo the tone in which you say "So your actual question is why the gcc compiler seems to optimize the code even though it is called with no options and no optimizations enabled (equal to -O0)?" is not good. why hung up on such a small thing when all I did was asking answer for my curiosity. everyone answered & i got things cleared.as simple as that – anekix Feb 22 '18 at 15:10
  • Your godbolt link doesn't take you to the code in your question. – Michael Petch Feb 22 '18 at 15:12
  • @MichaelPetch link takes to the compiler & i have exact code mentioned in the question.also couldn't find anywhere on godbolt how to share the code. – anekix Feb 22 '18 at 15:14
  • 1
    In the upper right of the godbolt page in the purplelish/blue bar there is a `share` pull down menu. Select `full` (instead of short in the box that appears) and copy the link provided. – Michael Petch Feb 22 '18 at 15:15
  • @MichaelPetch thanks :) have edited the question – anekix Feb 22 '18 at 15:18
  • @anekix Because that would be a different (and more interesting) question. Namely how a compiler with optimizations disabled still performs some optimizations. While questions of the nature "how did I end up with this machine code" without even specifying if optimizations are enabled or not are very tiresome, as one usually can't reason about the effectiveness of non-optimized code in meaningful ways. – Lundin Feb 22 '18 at 15:20
  • @Lundin thanks i will keep this in mind next time :) – anekix Feb 22 '18 at 15:21

6 Answers6

7

You don't see it, because it isn't there. The compiler was able to perform analysis, and rather easily see that this branch will always be entered.

Instead of emitting a check that will do nothing but waste CPU cycles, it emits an easily optimized version of the code.

A C program is not a sequence of instructions for the CPU to perform. That's what the emitted machine code is. A C program is a description of the behavior your compiled program should have. A compiler is free to translate it in almost any way it wants, so long as you get that behavior.

It's known as "the as-if rule".

StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
4

The interesting thing here is that gcc and clang optimize away the if() even at -O0, unlike some other compilers (ICC and MSVC).

gcc -O0 doesn't mean no optimization, it means no extra optimization beyond what's needed to compile at all. But gcc does have to transform through a couple internal representations of the function logic before emitting asm. (GIMPLE and Register Transfer Language). gcc doesn't have a special "dumb mode" where it slavishly transliterates every part of every C expression to asm.

Even a super-simple one-pass compiler like TCC does minor optimizations within an expression (or even a statement), like realizing that an always-true condition doesn't require branching.

gcc -O0 is the default, which you obviously used because the dead store to zz isn't optimized away.

gcc -O0 aims to compile quickly, and to give consistent debugging results.

  • Every C variable exists in memory, whether it's ever used or not.

  • Nothing is kept in registers across C statements (except variables declared register; -O0 is the only time that keyword does anything). So you can modify any C variable with a debugger while single-stepping. i.e. spill/reload everything between separate C statements. See also Why does clang produce inefficient asm with -O0 (for this simple floating point sum)? (This is why benchmarking for -O0 is nonsense: writing the same code with fewer larger expressions is faster only at -O0, not with real settings like -O3).

    Other interesting consequences: constant-propagation doesn't work, see Why does integer division by -1 (negative one) result in FPE? for a case where gcc uses div for a variable set to a constant, vs. something simpler for a literal constant.

  • Every statement is compiled independently, so you can even jump to a different source line (within the same function) using GDB and get consistent results. (Unlike in optimized code where that would be likely to crash or give nonsense, and definitely not match the C abstract machine).

Given all those requirements for gcc -O0 behaviour, if (2 < 3) can still be optimized to zero asm instructions. The behaviour doesn't depend on the value of any variable, and it's a single statement. There's no way it can ever be not-taken, so the simplest way to compile it is no instructions: fall-through into the { body } of the if.

Note that gcc -O0's rules / restrictions go far beyond the C as-if rule that the machine-code for a function merely has to implement all externally-visible behaviour of the C source. gcc -O3 optimizes the whole function down to just

main:                 # with optimization
    xor    eax, eax
    ret

because it doesn't care about keeping asm for every C statement.


Other compilers:

See all 4 of the major x86 compilers on Godbolt.

clang is similar to gcc, but with a dead store of 0 to another spot on the stack, as well as the 10 for zz. clang -O0 is often closer to a transliteration of C into asm, for example it will use div for x / 2 instead of a shift, while gcc uses a multiplicative inverse for division by a constant even at -O0. But in this case, clang also decides that no instructions are sufficient for an always-true condition.

ICC and MSVC both emit asm for the branch, but instead of the mov $2, %ecx / cmp $3, %ecx you might expect, they both actually do 0 != 1 for no apparent reason:

# ICC18
    pushq     %rbp                                          #6.1
    movq      %rsp, %rbp                                    #6.1
    subq      $16, %rsp                                     #6.1

    movl      $0, %eax                                      #7.5
    cmpl      $1, %eax                                      #7.5
    je        ..B1.3        # Prob 100%                     #7.5

    movl      $10, -16(%rbp)                                #9.16
..B1.3:                         # Preds ..B1.2 ..B1.1
    movl      $0, %eax                                      #11.12
    leave                                                   #11.12
    ret                                                     #11.12

MSVC uses the xor-zeroing peephole optimization even without optimization enabled.

It's slightly interesting to look at which local / peephole optimizations compilers do even at -O0, but it doesn't tell you anything fundamental about C language rules or your code, it just tells you about compiler internals and the tradeoffs the compiler devs chose between spending time looking for simple optimizations vs. compiling even faster in no-optimization mode.

The asm is never intended to faithfully represent the C source in any kind of way that would let a decompiler reconstruct it. Just to implement equivalent logic.

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
  • @anekix: you might be interested in my edit: MSVC and ICC do compare-and-branch, but on `0 != 1` instead of `2 < 3`. Presumably any always-true condition turns into a branch on `0 != 1` with no-optimization for those compilers. – Peter Cordes Feb 22 '18 at 17:42
  • 1
    TCC will optimize away the comparison but it won't optimize away the unused variable `zz` and will still initialize it to 10. TCC's optimizer operates on expressions only. So it can realize the expression in the `if` is always true and optimize away the `if` statement itself. – Michael Petch Feb 22 '18 at 19:58
  • @MichaelPetch: thanks, IDK why I said TCC might not optimize even within a single expression. Fixed. – Peter Cordes Feb 22 '18 at 20:04
2

It's simple. It is not there. The compiler optimized it away.

Here is the assembly when compiling with gcc without optimization:

    .file   "k.c"
    .text
    .globl  test
    .type   test, @function
test:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    nop
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   test, .-test
    .globl  main
    .type   main, @function
main:
.LFB1:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    $10, -4(%rbp)
    movl    $0, %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE1:
    .size   main, .-main
    .ident  "GCC: (Debian 6.3.0-18) 6.3.0 20170516"
    .section    .note.GNU-stack,"",@progbits

and here it is with optimization:

    .file   "k.c"
    .text
    .p2align 4,,15
    .globl  test
    .type   test, @function
test:
.LFB11:
    .cfi_startproc
    rep ret
    .cfi_endproc
.LFE11:
    .size   test, .-test
    .section    .text.startup,"ax",@progbits
    .p2align 4,,15
    .globl  main
    .type   main, @function
main:
.LFB12:
    .cfi_startproc
    xorl    %eax, %eax
    ret
    .cfi_endproc
.LFE12:
    .size   main, .-main
    .ident  "GCC: (Debian 6.3.0-18) 6.3.0 20170516"
    .section    .note.GNU-stack,"",@progbits

As you can see, not only the comparison is optimized away. Almost the whole main is optimized away since it does not produce anything visible. The variable zz is never used. The only observable thing your code does is returning 0.

klutt
  • 30,332
  • 17
  • 55
  • 95
1

2 is always less tan 3 so, as the compiler know the result of 2<3 is always true, there is no need for an if decision in assembler.

The optimization means to generate less time / less code.

J_P
  • 761
  • 8
  • 17
0
if (2<3)

is allways true, therefore the Compiler emmits no opcode for it.

notan
  • 359
  • 1
  • 10
0

The condition if (2<3) is always true. So a decent compiler would detect this generate the code as if the condition doesn't exist. In fact, if you optimize it with -O3, godbolt.org generates just:

test():
  rep ret
main:
  xor eax, eax
  ret

This is again valid because a compiler is allowed optimise and transform the code as long as the observable behaviour is preserved.

P.P
  • 117,907
  • 20
  • 175
  • 238
  • The interesting thing is that it optimizes away the `if()` *even at `-O0`* like the OP showed code for. `gcc -O0` doesn't mean no optimization, it means no *extra* optimization, and put memory barriers between every statement, so variables can be modified in memory with a debugger. Or you can use GDB's `jump` to any source line inside the same function. But none of these things require any instructions to be emitted for a comparison between two compile-time constants. – Peter Cordes Feb 22 '18 at 16:50