Module 2 Lessons — ISA & Microarchitecture

Back to the Course 3 syllabus · Practice: Module 2 exercises

This page covers the two ideas that organize everything else in the course: the ISA — the contract between software and hardware — and the microarchitecture — how a particular chip honors that contract, from a single-cycle toy to the pipelined, out-of-order, cache-fed machine you’ll measure on the M-series. It condenses H&H 6–8; the H&H problem sets remain Exercise 2.1’s by-hand work.


1 · What an ISA is

The instruction set architecture defines everything software can observe: the registers, the instruction behaviors and encodings, the memory model and addressing. It is a contract: any microarchitecture that honors it runs the same binaries — a 3-stage in-order Cortex-M4 and a huge out-of-order Apple core can both be “ARM” while sharing almost no implementation.

Design ingredients every ISA chooses (H&H 6.2–6.4):

  • Register count/width — more registers = fewer memory trips, but more encoding bits per operand.
  • Load/store vs. memory-operand — RISC ISAs (all three ARMs here) touch memory only through loads/stores; arithmetic is register-to-register.
  • Encoding regularity — A64 instructions are all 32 bits: trivial to fetch/decode in parallel; the cost is that constants and addresses don’t fit in an instruction (hence MOVK chains and ADRP, Modules 0 & 3).
  • Addressing modes — how loads/stores form addresses (Module 3 §5 catalogs A64’s).

One theory, three ARM dialects

This course reads all three on purpose — the concepts transfer, the syntax doesn’t:

A32 (H&H’s dialect) T32 / Thumb-2 (Cortex-M) A64 (your Mac)
Registers R0–R15 (PC is R15) R0–R15, same X0–X30 + SP; PC not a GP register
Encoding fixed 32-bit mixed 16/32-bit — code density fixed 32-bit
Conditional execution on nearly every instruction (4-bit cond field) via short IT blocks branches + CSEL family only
Typical world classic 32-bit ARM microcontrollers (STM32) phones/laptops/servers

The A32 habit of predicating everything and spending R15-as-PC tricks doesn’t survive into A64 — the modern replacement for small conditionals is CSEL (conditional select), which Exercise 2.5 shows beating branches on unpredictable data. Exercise 2.2’s three-dialect table makes these trade-offs concrete on one routine.

Machine encoding (H&H 6.4): fields for opcode, registers, immediates packed into the 32-bit word. You never memorize encodings, but knowing they’re fields explains real limits: why immediates have odd ranges (12-bit, optionally shifted), why branch reach is finite (±128 MB for B), why MOV x0, #big_constant becomes a MOVZ/MOVK sequence. When objdump shows bytes next to mnemonics, that’s the fields.

2 · Performance analysis: the iron law

\[\text{Time} = \frac{\text{instructions}}{\text{program}} \times \frac{\text{cycles}}{\text{instruction}} \times \frac{\text{seconds}}{\text{cycle}}\]

Every optimization in this course attacks one factor, usually paying in another: the compiler shrinks instruction count; pipelining shrinks the cycle time; hazards and cache misses inflate CPI; superscalar issue pushes CPI below 1. When Exercise 2.4–2.6 measure your M-series, you’re measuring CPI effects — instruction count and clock are fixed by the binary and the chip.

3 · Single-cycle → multicycle → pipelined

Single-cycle (H&H 7.3): every instruction completes in one clock. The datapath — PC → instruction memory → register file → ALU → data memory → writeback — is a straight combinational shot, so the clock must fit the slowest instruction (a load’s full journey). Simple; wasteful.

Multicycle (7.4): chop execution into steps, reuse one ALU/one memory across cycles, let each instruction take only the cycles it needs. Better hardware economy; the clock is now set by the slowest step.

Pipelined (7.5): the real answer — overlap. Registers between stages let five instructions occupy five stages at once:

flowchart LR
    F["IF<br/>fetch"] --> D["ID<br/>decode +<br/>register read"] --> E["EX<br/>ALU"] --> M["MEM<br/>load/store"] --> W["WB<br/>writeback"]

flowchart LR
    F["IF<br/>fetch"] --> D["ID<br/>decode +<br/>register read"] --> E["EX<br/>ALU"] --> M["MEM<br/>load/store"] --> W["WB<br/>writeback"]

Throughput approaches one instruction per cycle at a clock set by the slowest stage (Module 1 §3’s timing constraint, now per-stage). Ideal CPI = 1. Reality intrudes as hazards:

  • Data hazard (RAW): an instruction needs a result still in flight. Cure #1: forwarding — route results from EX/MEM stage outputs straight back to the ALU inputs, skipping the register file. Cure #2, when forwarding can’t help: stall.
    • The unavoidable one: the load-use stall. A load’s data appears after MEM; a dependent instruction in the very next slot must wait one cycle. Compilers schedule an independent instruction into the slot when they can — one thing to look for in Exercise 2.3’s -O2 output.
  • Control hazard: a branch’s outcome is known deep in the pipe, but fetch must continue somewhere. Guess wrong and everything fetched after the branch is flushed. Cure: branch prediction (§4).
  • (Structural hazards — two stages wanting one resource — are designed out with separate instruction/data caches and enough ports.)

Deeper pipelines raise the clock but raise the flush cost too; that tension shapes everything in §4.

4 · Beyond the textbook pipeline: the machine you actually own

H&H 7.7’s “advanced microarchitecture” section is a sketch of your Mac:

  • Branch prediction: dynamic predictors learn per-branch history (taken/not-taken counters, then pattern- and target-history). Modern predictors are startlingly good on regular patterns and helpless on random ones — which is precisely Exercise 2.5: sorted data ≈ free branches; shuffled data ≈ a pipeline flush every other element; CSEL sidesteps prediction entirely by turning control into data.
  • Superscalar: fetch/decode/issue several instructions per cycle (CPI < 1).
  • Out-of-order execution: hardware renames registers, tracks true data dependencies, and executes whatever is ready — the dependence graph, not program order, is the schedule. Independent chains fill the machine; one long dependent chain leaves it idle. Exercise 2.6’s two loops (one chain vs. eight interleaved) measure exactly this — expect several-fold throughput difference from identical instruction counts.
  • The M-profile contrast to hold onto: the Cortex-M4 is a 3-stage, in-order, no-prediction-to-speak-of machine. On it, instruction count ≈ time, and none of the dynamic magic above exists — the mental model for Course 2’s cycle budgets is simpler, and Module 6 leans on that.

5 · Memory systems I: caches

The performance problem: cores execute in fractions of a nanosecond; DRAM answers in ~100 ns. The fix exploits locality — temporal (recently used → soon reused) and spatial (near a used address → soon used) — with small fast SRAM close to the core:

Geometry. A cache holds blocks/lines (64 bytes on the M-series — spatial locality’s unit). An address splits into tag | index | offset: the index picks a set, the tag disambiguates which memory block sits there, the offset selects the byte. Direct-mapped = 1 block/set (fast, conflict-prone); N-way set-associative = N candidates per set (the norm); LRU-ish replacement picks the victim. Writes: write-back with write-allocate is the modern default (dirty lines written to the next level on eviction).

Accounting.

\[\text{AMAT} = t_{\text{hit}} + \text{miss rate} \times t_{\text{miss penalty}}\]

applied per level: L1 (~few cycles) → L2 (~a dozen+) → shared cache → DRAM (~100+ ns). The miss taxonomy — compulsory (first touch), capacity (working set too big), conflict (set collisions) — tells you which knob would have helped.

Exercise 2.4’s pointer-chase reads this hierarchy directly: dependent loads defeat both prediction and out-of-order overlap, so ns/load vs. working-set size traces a staircase whose steps are your L1/L2/SLC/DRAM latencies. Predict the plateau positions from public M-series specs first; reconciling the plot against the prediction is the module’s centerpiece.

Two software corollaries that Course 2 inherits: stride-1 loops are fast because of lines, not luck; and a working set that fits in L1/L2 is a design decision you can make on purpose (sample-block sizes, filter state layout).

6 · Memory systems II: virtual memory

Every process on your Mac sees a private flat address space; the OS + hardware translate virtual → physical page by page (16 KB pages on Apple platforms):

  • Page tables hold per-page translations + permissions (read/write/execute — why data isn’t executable and code isn’t writable).
  • The TLB caches translations; a TLB miss walks the table in hardware. Huge working sets can miss in the TLB before they miss in cache — a second staircase hiding in Exercise 2.4’s largest sizes.
  • Page faults let the OS map pages lazily, share libraries, and swap — dyld’s mapping of libSystem (Module 0) rides on all of this.

The M-profile contrast: the Cortex-M has no MMU and no virtual memory — one physical address map, optionally an MPU enforcing a handful of protection regions (Module 6 §5). No page tables, no TLB, no faults-as-a-service: another reason firmware timing is analyzable in a way desktop timing never is.

Where the exercises hook in

Lesson section Exercise
All (by-hand analysis) 2.1 — H&H problem sets
§1 three dialects 2.2 — one routine, three dialects
§2–3 what compilers do with the pipeline 2.3 — compiler forensics at -O0/-O2
§5–6 hierarchy 2.4 — cache-latency ladder
§4 prediction vs. CSEL 2.5 — branch predictor experiment
§4 out-of-order 2.6 — ILP probe

Reference shelf: H&H 6–8 (problem sets; App. B as the A32 instruction reference); Yiu 4, 5.1–5.6, 6.1–6.6 for the M-profile skim (deep-read comes in Module 6); Plantz 8 (memory hierarchy) and 9 (CPU subsystems, A64 registers) retell §5 and §1 gently.