How if/else Compiles
Translating an if/else into a compare followed by a conditional jump over a block.
Putting the Pieces Together
You now have all three ingredients: a cmp that sets the flags, a conditional jump that reads them, and an unconditional jmp that stitches blocks together. An if/else is exactly these three things arranged into a recognisable shape. Once you have seen the shape a few times, you can read any branch at a glance.
Let us build it up from the simplest case.
An if With No else
Start with a function that returns the absolute value of its argument:
int absValue(int n)
{
if (n < 0)
return -n;
return n;
}
At -O0:
absValue(int):
push rbp
mov rbp, rsp
mov DWORD PTR -4[rbp], edi
cmp DWORD PTR -4[rbp], 0
jns .L2
neg DWORD PTR -4[rbp]
.L2:
mov eax, DWORD PTR -4[rbp]
pop rbp
ret
Read it as a unit. The source says "if n is negative, negate it". The compiler did the opposite: it compares n against 0 and uses jns ("jump if not negative") to skip over the body of the if. So the neg instruction, which is the return -n path, only runs when n is negative; otherwise jns jumps straight to .L2 and leaves n alone. The label .L2 marks "the code after the if".
This is the canonical pattern for an if with no else:
cmp / test ; evaluate the condition
j<inverted> .Lafter ; if condition is FALSE, skip the body
... body ... ; the "then" block
.Lafter: ; execution continues here either way
The jump always tests the inverted condition, because its job is to jump past the body when the body should not run.
Adding the else
Now a real two-way branch:
int classify(int score)
{
if (score >= 50)
return 1;
else
return 0;
}
classify(int):
push rbp
mov rbp, rsp
mov DWORD PTR -4[rbp], edi
cmp DWORD PTR -4[rbp], 49
jle .L2
mov eax, 1
jmp .L3
.L2:
mov eax, 0
.L3:
pop rbp
ret
Trace the two paths:
- Condition true (
score >= 50):jleis not taken (the value is not<= 49), so we fall through tomov eax, 1(thethenblock, returning 1). Thenjmp .L3leaps over theelseblock. - Condition false (
score < 50):jleis taken, jumping to.L2, which runsmov eax, 0(theelseblock, returning 0).
The shape of an if/else is therefore:
cmp / test ; evaluate the condition
j<inverted> .Lelse ; if FALSE, jump to the else block
... then block ... ; runs when condition is true
jmp .Lend ; skip over the else block
.Lelse:
... else block ... ; runs when condition is false
.Lend:
The two labels do all the work. .Lelse is where the false path lands; .Lend is where both paths reconverge. The lone jmp .Lend at the bottom of the then block is the giveaway that there is an else: a bare if (the previous example) has no such jump because there is no second block to skip.
Reading Direction: Source vs Assembly
The most disorienting thing for newcomers is that the order of blocks in the assembly does not always match the source. The then block usually comes first (it falls through), and the else block sits lower, reached by a taken jump. The compiler is free to lay the blocks out in whatever order produces the fewest or fastest jumps, so do not assume "first block in the listing" equals "first branch in the source". Follow the labels and the inverted condition instead, and you will always recover the original logic.
When the Branch Disappears Entirely
Sometimes there is no jump at all. Consider a max function:
int maxOf(int a, int b)
{
if (a > b)
return a;
else
return b;
}
At -O2, GCC produces this:
maxOf(int, int):
cmp edi, esi
mov eax, esi
cmovge eax, edi
ret
There is a cmp, but no conditional jump. Instead you see cmovge, a conditional move. It copies edi into eax only if the "greater than or equal" condition holds, and does nothing otherwise. The function first assumes the answer is b (mov eax, esi), then overwrites it with a if a >= b.
Why bother? A jump that the CPU guesses wrong is expensive, because the processor speculatively runs ahead and must throw away that work when the guess fails. A cmov has no branch to mispredict: it always executes, and simply chooses whether to keep the result. For small, unpredictable if/else blocks the optimizer often prefers this branchless form. Seeing cmov in a listing is your cue that an if/else was flattened into straight-line code, which is exactly the kind of transformation that makes reading optimized assembly worthwhile.
Key Takeaways
- An
ifwith noelseis:cmp/test, a jump on the inverted condition that skips the body, then the after-label. - An
if/elseadds anelselabel and a trailingjmpat the end of thethenblock to reconverge at an end-label. - The conditional jump almost always tests the opposite of the source condition, because its purpose is to jump past the block that should not run.
- Block order in assembly need not match source order; follow the labels and inverted condition, not physical position.
- At higher optimization levels a small
if/elsecan become branchless usingcmov(conditional move), which has no jump to mispredict.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
How if/else Compiles - Quiz
Test your understanding of the lesson.
Practice Exercises
Fix the Condition Behind the Branch
The classify function should return 1 for a passing score (50 or more) and 0 otherwise, but its comparison operator is wrong, so every result is flipped. The program prints classify(72), classify(50), and classify(31). Fix the condition, then read the assembly panel and find the cmp that evaluates the score, the inverted conditional jump (jle) that jumps over the then-block, and the unconditional jmp that skips the else-block.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!