Control Flow at Scale

x86-64 Assembly

Chapter 6 · Control Flow at Scale

Jumps and loops will feel familiar after two full prior courses of them. This chapter covers that familiar ground quickly, then introduces something genuinely new: a way to make a decision without ever branching the instruction pointer at all.

Unconditional and Conditional Jumps

JMP is unconditional, the same idea as the 6502's own JMP or LC-3's BRnzp. The conditional Jcc family reads RFLAGS the same way assembly2-3 already covered — including JC/JNC, a direct parallel to cpu8bit1-8's own 6502 BCC and cpu8bit1-12's own Z80 JR NC, now with the flag convention already settled in assembly2-3.

JG and JA are NOT the same "greater than"
x86-64 deliberately provides two separate families of comparison jumps: JG/JL (and friends) use signed comparison, while JA/JB (and friends) use unsigned comparison — genuinely different conditions, even though both read as "jump if greater/less" in plain English. Using JG on data meant to be treated as unsigned (or vice versa) is a real, common bug: the two families can disagree on the exact same bit pattern depending on whether it's interpreted as a negative signed number or a large unsigned one.

LOOP — A DJNZ-Style Instruction

x86-64 has a native LOOP instruction: decrement RCX, and jump if RCX isn't zero — one instruction, doing exactly what cpu8bit1-8's own Z80 DJNZ does.

MOV RCX, 5
LOOP_START
        ...                ; loop body
        LOOP LOOP_START  ; RCX-- ; jump to LOOP_START if RCX != 0
Modern code mostly avoids LOOP anyway
Despite being just as compact and elegant as DJNZ, real modern x86-64 code rarely uses LOOP — on modern CPU microarchitectures, an explicit DEC RCX / JNZ pair is typically executed faster than the single LOOP instruction, because modern internal pipelining optimizes the two ordinary instructions better than it optimizes LOOP itself. A genuinely elegant, DJNZ-like instruction, deliberately avoided in practice for real performance reasons — one more instance of the "richness doesn't automatically mean faster" theme cpu8bit1-7 and cpu8bit1-8 both already established.

CMOV — A Genuinely New Idea: Branchless Code

Nothing in assembly1 or cpu8bit1 offered this: CMOVcc conditionally moves a value based on the current flags — but the instruction itself always executes, with no branch taken either way. A decision, without ever redirecting the instruction pointer.

CMP  RAX, RBX
CMOVL RAX, RBX   ; if RAX < RBX (signed), RAX becomes RBX — branchless max

Compare this directly against cpu8bit1-12's own capstone, which found a maximum using an explicit CMP/branch/STA sequence on both the 6502 and Z80 — real branches, genuinely taken or not taken. CMOVL computes the identical logical result without a single conditional jump anywhere in the sequence.

Why this matters: modern CPUs rely heavily on branch prediction and deep instruction pipelining (concepts this course hasn't needed until now) to run fast. A mispredicted branch forces the CPU to discard speculative work and refill its pipeline — a real, measurable cost. For simple "if condition, then set this value" patterns, CMOV sidesteps misprediction risk entirely, since there's no branch to mispredict in the first place.

CMOV isn't automatically the faster choice either
CMOV always does the conditional-move work, every single time, regardless of which way the condition actually goes — where a real branch, correctly predicted (which a modern branch predictor manages the large majority of the time for genuinely predictable patterns), can skip the "not taken" path's cost entirely. For a condition that's rarely true and easy for hardware to predict, an ordinary branch can still outperform CMOV. This is exactly the same honest nuance cpu8bit1-7's own cycle-cost table and cpu8bit1-8's own clock-speed caveat already established: more capability is not the same claim as automatically faster.
TechniqueLC-3 / 6502 / Z80x86-64
Simple conditional branchBR family / Jcc-equivalent branchesJcc — same idea, richer signed/unsigned condition set
Decrement-and-loopassembly1-6's BR loop, cpu8bit1-8's INX+CPX+BNE, or DJNZLOOP — conceptually the same as DJNZ, but often avoided on modern hardware
Conditional assignmentAlways required an actual branchCMOV — genuinely branchless, a technique none of the prior three offered
A cleaner tool for the capstone
assembly2-12's own capstone could implement its own max-finding logic using CMOV instead of an explicit branch — the exact same task cpu8bit1-12's capstone solved with branching on two different chips, now solvable with no branch at all.

Hands-On Exercises

Exercise 1

Explain the difference between JG and JA, using this chapter's own signed-vs-unsigned distinction, and explain why using the wrong one for a given data type is a real, documented bug source rather than just a style preference.

📄 View solution
Exercise 2

Trace this chapter's own branchless-max example — CMP RAX, RBX then CMOVL RAX, RBX — given RAX = 10 and RBX = 25 beforehand. State RAX's final value and explain each step.

📄 View solution
Exercise 3

Explain why LOOP, despite being conceptually similar to cpu8bit1-8's own celebrated Z80 DJNZ, is often avoided in real modern x86-64 code — and explain why this chapter treats that fact as a further instance of the site's own recurring "richness doesn't automatically mean faster" theme.

📄 View solution

Chapter 6 Quick Reference

  • JMP — unconditional; Jcc family — conditional, reading RFLAGS per assembly2-3
  • JG/JL vs. JA/JB — signed vs. unsigned comparison, a real and common source of bugs when mismatched to the data
  • LOOP — decrement RCX and jump if nonzero, one instruction, conceptually matching cpu8bit1-8's own DJNZ
  • Modern code often prefers explicit DEC+JNZ over LOOP for real microarchitectural performance reasons
  • CMOVcc — conditionally moves a value with no branch at all — a genuinely new technique, sidestepping branch-misprediction cost
  • CMOV isn't automatically faster than a real branch — it always does its own work regardless of the condition, unlike a correctly-predicted skipped branch
  • The capstone (assembly2-12) can revisit cpu8bit1-12's own max-finding task using CMOV instead of branching