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 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
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 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.
| Technique | LC-3 / 6502 / Z80 | x86-64 |
|---|---|---|
| Simple conditional branch | BR family / Jcc-equivalent branches | Jcc — same idea, richer signed/unsigned condition set |
| Decrement-and-loop | assembly1-6's BR loop, cpu8bit1-8's INX+CPX+BNE, or DJNZ | LOOP — conceptually the same as DJNZ, but often avoided on modern hardware |
| Conditional assignment | Always required an actual branch | CMOV — genuinely branchless, a technique none of the prior three offered |
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
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 solutionTrace 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.
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 solutionChapter 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