Reading Assembly

Part 1 · The register machine

Control flow is compare-and-jump

There are no booleans and no if statements. There is a register of flag bits, and instructions that read them — and sometimes the branch disappears entirely.

35 minPOEbeacon trainingcontrasting cases
What this lesson is trying to break (3)
  • A comparison produces a boolean value.
  • Every `if` in the source becomes a branch in the output.
  • Branchless code is always faster.

There is no if at this level, and there are no booleans. There is a small register of individual bits, and a set of instructions that consult it.

The flags

Arithmetic and comparison instructions set flags as a side effect. The four that matter:

Flag Set when
ZF zero the result was zero
SF sign the result’s top bit was set (negative, if signed)
CF carry an unsigned overflow occurred
OF overflow a signed overflow occurred

cmp a, b performs a - b, throws the result away, and keeps the flags. test a, b performs a AND b, likewise. Then a conditional instruction reads some combination.

This is why lesson 1.2 could say that signedness lives in the jump: jb (below) consults CF, while jl (less) consults SF ≠ OF. Same cmp, different flags read, different type semantics.

Beaconsee this → think this, without decoding it

test rax, rax · test eax, eax

A zero or null check.

rax AND rax is just rax, so this is a pure flag-setting move — it says “is this zero, and what is its sign?” without needing a constant operand.

Followed by je/jz it is if (x == 0). Followed by jne/jnz it is if (x). On a pointer it is a null check. This is one of the highest-frequency patterns in all compiled code, and recognising it instantly removes a lot of friction.

The branch that isn’t there

Now the part that breaks the naive model.

Predict firstcommit in writing, then look
int a(int x) {
    return x < 0 ? -x : x;
}

An absolute value. There is clearly a conditional in the source.

But branchless is not free

The obvious next thought is “so always go branchless”. That is wrong, and knowing why is the useful part.

Varies: only how predictable the condition is

Branchcmov / csel
Work doneOnly the taken sideBoth sides, always
Well-predicted condition~0 cycles — prediction hides itPays for both sides every time
Unpredictable condition~15 cycles on a mispredict~1–2 cycles, always
Data dependencyNone — speculation runs aheadYes — must wait for the condition
Can it skip expensive work?YesNo — both sides are evaluated

A branch predicted correctly 99% of the time is nearly free, and cmov would be a pure loss. A branch that is genuinely 50/50 is expensive, and cmov wins. The compiler guesses; profile-guided optimisation lets it know.

Two consequences worth carrying:

A cmov cannot be used if either side has side effects or might fault. You cannot conditionally-move a memory load that would segfault, because it would be performed. That is why if (p) use(*p) must stay a branch.

cmov creates a data dependency. The result waits on the condition, so inside a dependency chain a cmov can be slower than a predicted branch even when the branch occasionally misses. This is the first place fiction 2 starts leaking, and it is dealt with properly in 4.3.

ARM64: the same idea, more of it

ARM64 calls it csel — conditional select — and it is three-operand, so it reads better:

csel    w0, w1, w2, lt      ; w0 = (lt) ? w1 : w2

ARM64 also has a family of related instructions — cinc (conditional increment), cneg, csetm, cset — so it can express more conditional shapes without branching. Compile the abs example for ARM64 and you will typically see something even shorter than the x86 version, because negate-and-select is a pattern the ISA anticipates.

Explain it to yourselfwrite it — thinking it does not count

For each, say whether you would expect a branch or a conditional move, and why:

  1. return x > y ? x : y; on random integers
  2. if (unlikely_error_flag) log_and_abort();
  3. if (ptr) return *ptr; else return 0;
  4. count += (c == ' '); inside a loop over English text

Number 3 has a forced answer for a reason other than prediction. Say what it is.

What you should now be able to say

  • There are no booleans. Comparisons set flags; separate instructions read them.
  • cmp is a subtract that keeps only flags; test rax, rax is the zero/null check idiom.
  • Signedness lives in which flags the conditional reads.
  • cmov/csel means the compiler removed a branch, computing both sides and discarding one.
  • Branchless is not automatically better: it always pays for both sides, creates a data dependency, and cannot be used where a side might fault.
  • ARM64’s csel family covers more conditional shapes than x86’s cmov.

Next: 1.5 — Reading your first function.

In the wildgo read the real thing now, while it is fresh
  • x86 SETcc and CMOVccThe full condition list. Read it alongside the jump table from 1.2 — they use identical suffixes, because they read identical flags.
Scheduled for recall3 items added to your review queue

These are not shown to you now on purpose. They will surface in Review tomorrow, mixed in among items from other parts of the course — not grouped by topic. Sorting out which idea applies is the part that transfers.