Skip to content
Free public beta until December 4, 2026: every course is open, no card needed. After that, the first chapter of each course stays free and full access is a one-time $39 ($19 for up to 100 founding members chosen for helpful feedback). Your progress is kept. How the beta works

Every way to branch or choose a value

Compare and branch, compare-and-branch in one instruction, cmov/csel/setcc without branching, a jump table for a switch, and lane masks and blends inside SIMD. Predictability and the number of cases decide.

Ask what is being chosen and how predictable the choice is.

  • Run different code depending on a test → a conditional branch: cmp/test then jcc on x86-64, cmp then b.cond on ARM64, or one compare-and-branch (blt, bltu, … on RISC-V; cbz, cbnz, tbz on ARM64 when testing for zero or one bit). Pick the signed or unsigned condition from the C++ types.
  • Choose between two values and the test is hard to predict (random data) → branchless: cmovcc/setcc on x86-64, csel/ csinc/cset on ARM64, a mask-and-blend on RISC-V. Both values are computed either way, so this loses when one side is expensive or the branch is predictable.
  • Many cases of one integer (a dense switch) → a jump table: one bounds check and one indirect jump.
  • A different target known only at run time (virtual call, function pointer) → an indirect call through a register or a vtable.
  • The choice is made per element of a vector → no branch at all: a SIMD compare makes a lane mask, then a blend (or AVX-512 / SVE predicate) picks per lane.

When in doubt, write the plain if, let the compiler choose, and measure with real data: branch prediction makes most predictable branches nearly free.

Branch on the flags (or on two registers)

cmp and test set the flags and keep no result.

jl against jb, b.lt against b.lo, blt against bltu.

Formal. After a compare of a with b (a − b):

C++ test signed x86-64 unsigned x86-64 signed ARM64 unsigned ARM64 RISC-V (signed / unsigned)
a == b je je b.eq b.eq beq
a != b jne jne b.ne b.ne bne
a < b jl (SF ≠ OF) jb (CF = 1) b.lt (N ≠ V) b.lo (C = 0) blt / bltu
a <= b jle jbe b.le b.ls bge b, a / bgeu b, a
a > b jg ja b.gt b.hi blt b, a / bltu b, a
a >= b jge jae b.ge b.hs bge / bgeu

Signed conditions use the words less/greater (l, g, lt, gt); unsigned ones use below/above (b, a) on x86-64 and lower/higher (lo, hi, ls, hs) on ARM64. RISC-V's bgt, ble, bgtu, bleu are pseudo-instructions that swap the operands.

In plain English. The compare is the same either way; the branch decides whether the bits are read as signed or unsigned numbers. "Less" and "greater" mean signed; "below", "above", "lower" and "higher" mean unsigned. Equal and not-equal don't care.

Example. Compare −1 with 1. As signed, −1 < 1, so jl / b.lt / blt are taken. As unsigned, −1's pattern is the largest value, so jb / b.lo / bltu are not taken.

Why this rule exists. Two's complement lets one subtractor serve both kinds of number, so the ISA moves the signed-or-unsigned choice to the condition. The compiler picks it from the C++ types being compared.

The compiler branches on the opposite condition, around the then-part.

A compare and a branch in one instruction, for zero tests and single bits.

Choose a value without branching

cmov, setcc, csel, csinc, and the mask trick on RISC-V.

An x86-64 instruction that copies a value only if a condition holds, letting code choose between two values without jumping.

Example

mov eax, esi; cmp edi, esi; cmovg eax, edi is max(a, b) for int with no branch.
ARM64's csel, which picks one of two registers depending on the flags and writes it to a third, with no jump.

Example

cmp w0, w1; csel w0, w0, w1, gt is the same max on ARM64.

When branchless wins (unpredictable data) and when it loses (predictable branches, expensive sides).

What a wrong guess costs, which is what branchless code is buying back.

Many targets

A dense switch becomes a bounds check and one indirect jump.

An indirect call through a table chosen by the object's type.

Formal. For a call p->f(args) to the virtual function in vtable slot k, through a pointer p to a polymorphic object (vptr at offset 0, 8-byte slots):

  1. vptr ← [p] (load the object's vtable pointer);
  2. target ← [vptr + 8k] (load slot k);
  3. call target indirectly, with p passed as the hidden this argument.

x86-64 (System V): mov rax, [rdi] then call [rax + 8*k]. ARM64: ldr x8, [x0], ldr x8, [x8, #8*k], blr x8. RISC-V: ld t0, 0(a0), ld t0, 8*k(t0), jalr t0.

In plain English. Each object carries a hidden pointer to its class's table of function addresses. A virtual call reads that pointer, reads the right entry from the table, and calls whatever address it finds, so the object's real class decides which function runs.

Example. Base Shape declares virtual area() in slot 0 and virtual name() in slot 1. For s->name() with s in rdi, the compiler emits mov rax, [rdi] and call [rax + 8]. A Circle and a Square share this code but hold different vptrs, so different functions run.

Why this rule exists. The compiler cannot know an object's real class at a call through a base pointer. One table per class plus one pointer per object makes the run-time choice cost two loads and an indirect call.

Per lane, inside a vector

A vector compare writes a mask, not flags; a blend picks per lane.

Mask registers make every instruction conditional per lane.