AI EngineeringZero to ProductionHome·About·Contact
Developer Foundations · Chapter DF8

Computer organization

Open the box: the von Neumann design, the parts of a CPU, a fetch-decode-execute cycle you actually run, the memory hierarchy by order-of-magnitude latency, cache locality, and a taste of assembly.

⏱️ ~3 hours🧪 6 runnable labs🎯 Beginner→Industry

Learning objectives

  • Describe the von Neumann architecture and the stored-program idea.
  • Name the CPU's parts — ALU, registers, control unit, program counter — and their jobs.
  • Trace the fetch–decode–execute cycle and run a tiny instruction set in Python.
  • Order the memory hierarchy by approximate latency and explain why it exists.
  • Explain locality of reference and why row-major traversal is faster.
  • Read a few lines of assembly and map them to the CPU model.
🌱 Start here — from zeroA computer is a machine that fetches an instruction, does it, and repeats — billions of times a second. This lesson opens the box: the von Neumann design, the parts of a CPU, the fetch-decode-execute loop (which we actually simulate), the memory hierarchy, and why cache-friendly code runs faster. The tiny-CPU output is real; the cache timings are labelled as machine-dependent because they genuinely vary by hardware.
These are models, not siliconThe Python 'CPU' and 'cache' here are teaching models — they illustrate the concepts and run correctly, but they are not cycle-accurate simulations of a real processor. Latency figures are orders of magnitude, approximate, and vary by chip.

1 · The von Neumann architecture essential

The von Neumann architecture (1945) is the blueprint for almost every computer: a single memory holds both instructions and data (the stored-program idea), a CPU reads and executes them, and I/O connects to the outside world. Because code is just data in memory, a program can be loaded, replaced, even generated at runtime — that is why software is soft.

Input keyboard, net Memory code + data CPU ALU + control Output screen, disk
The von Neumann bottleneckBecause instructions and data share one memory bus, the CPU can stall waiting on memory. Caches (section 4) and clever pipelining exist largely to hide this bottleneck.

2 · Inside the CPU essential

PartJob
ALU (arithmetic logic unit)does the math & logic — the adders from df7 live here
Registersa few ultra-fast storage slots the ALU works on directly
Control unitdecodes each instruction and steers the other parts
Program counter (PC)holds the address of the next instruction
Instruction registerholds the instruction currently being executed
Busthe wires moving data between CPU, memory and I/O

Registers are to the ALU what your hands are to you: the only place work actually happens. Everything else is about getting data into a register, operating, and putting the result back.

3 · Fetch–decode–execute: simulate a tiny CPU advanced

The CPU runs one loop forever: fetch the instruction at the PC, decode what it means, execute it, advance the PC, repeat. Let's build a working (model) CPU with 5 instructions and run a program that computes (3 + 4) × 2 and stores it.

Fetch read at PC Decode what op? Execute do it PC += 1 next instr
Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
Python · a 5-instruction toy CPU (runs)
tiny_cpu.py# Instruction set: (op, arg). Registers: ACC (accumulator) + a small RAM.
#   LOAD  v  -> ACC = v
#   ADD   v  -> ACC = ACC + v
#   MUL   v  -> ACC = ACC * v
#   STORE a  -> RAM[a] = ACC
#   HALT     -> stop
program = [
    ("LOAD", 3),
    ("ADD", 4),
    ("MUL", 2),
    ("STORE", 0),
    ("HALT", None),
]

def run(program):
    PC, ACC, RAM = 0, 0, [None] * 8       # program counter, accumulator, memory
    while True:
        op, arg = program[PC]             # FETCH
        # DECODE + EXECUTE
        if op == "LOAD":  ACC = arg
        elif op == "ADD": ACC += arg
        elif op == "MUL": ACC *= arg
        elif op == "STORE": RAM[arg] = ACC
        elif op == "HALT": break
        print(f"PC={PC} {op:<5} arg={arg}  -> ACC={ACC}")
        PC += 1                            # advance
    return RAM

ram = run(program)
print("RAM[0] =", ram[0])
PC=0 LOAD  arg=3  -> ACC=3
PC=1 ADD   arg=4  -> ACC=7
PC=2 MUL   arg=2  -> ACC=14
PC=3 STORE arg=0  -> ACC=14
RAM[0] = 14
This is a real CPU in miniatureAdd a JMP instruction that sets the PC directly and you have branching and loops — enough to be Turing-complete. Every processor is this loop, just wider, pipelined, and running billions of times per second.

4 · The memory hierarchy intermediate

There is no single memory that is both huge and instant, so computers stack layers: tiny-and-fast at the top, huge-and-slow at the bottom. The CPU checks the fast layers first. The numbers below are approximate orders of magnitude and vary widely by hardware — treat them as ratios, not guarantees.

LevelTypical sizeApprox. latency (order of magnitude)Analogy
Registers~KB (dozens)< 1 ns (fraction of a cycle)in your hand
L1 cache~32–64 KB~1 nson your desk
L2 cache~256 KB–1 MBa few nsdesk drawer
L3 cache~ several MB~10 nsbookshelf
Main memory (RAM)GBs~100 nsdown the hall
SSD / diskTBs~10 µs – 10 msin another building
Why these ratios matterA RAM access is roughly two orders of magnitude slower than an L1 hit; a disk seek is orders of magnitude slower again. Programs feel fast when they keep the data they need in the fast layers — which is what locality (next) is about.

5 · Caching & locality of reference advanced

A cache keeps recently/nearby-used data in a fast layer, betting on locality: temporal (you'll reuse what you just used) and spatial (you'll use data next to what you just used). Caches load a whole cache line (e.g. 64 bytes) at once, so touching neighbours is nearly free — but jumping around memory pays the full RAM latency each time.

The canonical demonstration: summing a 2-D array row-major (along rows, the way it's laid out in memory) touches consecutive addresses and rides the cache lines; column-major jumps by a whole row each step and thrashes the cache. The code below measures both on your machine:

Python · measure row-major vs column-major (runs; timings vary)
locality.pyimport time

N = 2000
# a flat list acts as an N x N matrix, row-major: element (r, c) at r*N + c
matrix = [0] * (N * N)

def sum_row_major():
    s = 0
    for r in range(N):
        base = r * N
        for c in range(N):
            s += matrix[base + c]        # consecutive addresses (cache-friendly)
    return s

def sum_col_major():
    s = 0
    for c in range(N):
        for r in range(N):
            s += matrix[r * N + c]       # jumps N elements each step (cache-hostile)
    return s

for name, fn in (("row-major", sum_row_major), ("col-major", sum_col_major)):
    t0 = time.perf_counter()
    fn()
    print(f"{name}: {time.perf_counter() - t0:.3f} s")
# Row-major is typically faster. Exact numbers depend on your CPU,
# cache sizes, and Python build -- run it and compare the two.
Why no fixed numbers hereThe two times depend entirely on your CPU, cache sizes, and Python version, so printing specific seconds would be fabricated. When you run it you should see row-major come out faster (often noticeably) — that ratio, not any absolute number, is the lesson. In Python the effect is muddied by interpreter overhead; in C/NumPy on contiguous memory it is dramatic.

6 · A taste of assembly professional

High-level code compiles down to assembly — human-readable names for the CPU's raw instructions. Here is roughly what c = a + b becomes on a register machine (x86-flavoured pseudocode). Notice it is exactly our tiny CPU: move into a register, add, store back.

Assembly-flavoured pseudocode for c = a + b (illustrative)
add.asmmov  eax, [a]     ; load variable a from memory into register eax
add  eax, [b]     ; eax = eax + b   (the ALU does the add)
mov  [c], eax     ; store the register result back into variable c

Compare to our toy CPU: mov eax,[a] is LOAD, add eax,[b] is ADD, mov [c],eax is STORE. Real ISAs (x86, ARM, RISC-V) have hundreds of instructions, but the shape — move to register, compute, store — is the same one you simulated.

Where this shows upYou rarely write assembly, but you read it when profiling hot loops, debugging with a disassembler, or understanding why a compiler optimization (inlining, vectorization) sped code up. It's the ground truth beneath every language.

✓ Checkpoint — you can move on when you can…

  • Draw the von Neumann model and explain the stored-program idea and its bottleneck.
  • Name the ALU, registers, control unit and PC and say what each does.
  • Walk the fetch–decode–execute cycle through a few toy instructions.
  • Order registers → L1/L2/L3 → RAM → disk by approximate latency.
  • Explain why row-major traversal is cache-friendlier than column-major.
  • Map three lines of assembly onto the CPU model.

Knowledge check check yourself

✓ Knowledge check

In the fetch–decode–execute cycle, what does the program counter (PC) do, and how do loops and branches happen?

Show answer
The PC holds the address of the next instruction; after each instruction it normally advances to the following one. A jump/branch instruction overwrites the PC with a different address, so execution continues elsewhere — repeatedly jumping back gives a loop, and conditionally jumping gives an if/branch.
✓ Knowledge check

Two functions sum the same 2-D array; one goes row-by-row, the other column-by-column, and the row version is faster. Why — and why can't we print a fixed speed-up number?

Show answer
Row-major traversal walks consecutive memory addresses, so each cache line loaded serves many accesses (spatial locality); column-major jumps a full row each step, so it misses the cache and pays RAM latency repeatedly. We can't quote a fixed number because the speed-up depends on the CPU, cache sizes, array size and language/runtime — it's a machine-dependent ratio, not a constant.

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · Trace the PC by handBeginner

Context: Before simulating a CPU you should be able to predict the program counter's path on paper.

Your task: Given a 4-instruction straight-line program, write out the sequence of PC values and the final accumulator value for LOAD 5; ADD 10; ADD 10; HALT.

Requirements:

  • List the PC value at each step (0, 1, 2, 3)
  • Track the accumulator after each instruction
  • State the final ACC and where execution stops

💡 Hint: With no jumps the PC just increments by one each cycle until HALT.

Show solution
# PC=0 LOAD 5 -> ACC=5
# PC=1 ADD 10 -> ACC=15
# PC=2 ADD 10 -> ACC=25
# PC=3 HALT    -> stop
print('PC path: 0,1,2,3  final ACC = 25')

Straight-line code means the PC is just 0,1,2,3. The accumulator ends at 25; HALT stops the loop at PC=3.

Exercise 2 · Add a SUB instructionIntermediate

Context: Extending an instruction set is the most direct way to prove you understand decode/execute.

Your task: Take the toy CPU and add a SUB v instruction (ACC = ACC − v); run a program that computes 20 − 5 − 3.

Requirements:

  • Add one decode branch for SUB
  • Write a program: LOAD 20; SUB 5; SUB 3; HALT
  • Print the final accumulator (should be 12)

💡 Hint: It's one more elif in the execute step — mirror how ADD is handled.

Show solution
program = [('LOAD',20),('SUB',5),('SUB',3),('HALT',None)]
def run(prog):
    PC, ACC = 0, 0
    while True:
        op, arg = prog[PC]
        if   op=='LOAD': ACC = arg
        elif op=='ADD':  ACC += arg
        elif op=='SUB':  ACC -= arg
        elif op=='HALT': break
        PC += 1
    return ACC
print(run(program))   # 12

Decode/execute is just a dispatch on the opcode; adding an operation is adding one branch. 20 − 5 − 3 = 12.

Exercise 3 · Add JMP and write a loopAdvanced

Context: Jumps are what separate a calculator from a computer; implementing one gives you loops and conditionals.

Your task: Add JMP addr (set PC = addr) and JZ addr (jump if ACC == 0) to the toy CPU, then write a countdown loop from 3 to 0.

Requirements:

  • Implement JMP by assigning to PC and not auto-incrementing after it
  • Implement JZ (jump-if-zero) for the branch
  • Write a loop that decrements ACC until it hits 0
  • Print each ACC value on the way down

💡 Hint: After a taken jump, continue the loop so you don't also run the PC += 1.

Show solution
prog = [
    ('LOAD', 3),   # 0
    ('PRINT', None), # 1
    ('SUB', 1),    # 2
    ('JZ', 6),     # 3  if ACC==0 jump to HALT
    ('JMP', 1),    # 4  else loop back to PRINT
    ('NOP', None), # 5
    ('HALT', None),# 6
]
def run(p):
    PC, ACC = 0, 0
    while True:
        op, arg = p[PC]
        if   op=='LOAD':  ACC = arg
        elif op=='SUB':   ACC -= arg
        elif op=='PRINT': print('ACC =', ACC)
        elif op=='JZ':
            if ACC == 0: PC = arg; continue
        elif op=='JMP':   PC = arg; continue
        elif op=='HALT':  break
        PC += 1
run(prog)   # prints ACC = 3, then 2, then 1

It prints 3, 2, 1: after ACC reaches 0 the JZ fires and jumps to HALT before the next PRINT. JMP/JZ overwrite the PC and continue so the normal increment is skipped. Jumping back to PRINT builds a loop; JZ exits it — branching and iteration from two instructions.

Exercise 4 · Measure locality yourselfExpert

Context: Cache effects are invisible until you measure them; a clean micro-benchmark is a real engineering skill (and a common interview probe).

Your task: Time row-major vs column-major summation of an N×N matrix and report the ratio, being explicit that absolute numbers are machine-dependent.

Requirements:

  • Build an N×N array (flat list, row-major layout)
  • Time both traversal orders with time.perf_counter
  • Print the ratio col-major/row-major, not fabricated absolute seconds
  • State clearly that the numbers vary by machine and that row-major should win

💡 Hint: Warm up once before timing, and keep N large enough (e.g. 2000) that the array exceeds cache.

Show solution
import time
N = 2000
m = [0] * (N*N)
def row():
    s = 0
    for r in range(N):
        b = r*N
        for c in range(N): s += m[b+c]
    return s
def col():
    s = 0
    for c in range(N):
        for r in range(N): s += m[r*N+c]
    return s
row(); col()                       # warm up
t=time.perf_counter(); row(); tr=time.perf_counter()-t
t=time.perf_counter(); col(); tc=time.perf_counter()-t
print(f'row {tr:.3f}s  col {tc:.3f}s  ratio {tc/tr:.2f}x')
# absolute times are machine-dependent; expect ratio > 1 (row faster)

Report the ratio, never a hard-coded absolute time — the point is that column-major pays repeated cache misses. In pure Python the interpreter overhead shrinks the gap; the same experiment in C or on a NumPy contiguous array shows a much larger effect.

Exercise 5 · Reorder a hot loop for the cacheProfessional

Context: A frequent real optimization is swapping loop order so memory is walked contiguously — a one-line change that can multiply throughput.

Your task: You inherit a function that copies a matrix column-by-column and is slow. Rewrite it to be cache-friendly and explain the fix.

Requirements:

  • Identify why the original (column-outer) order is cache-hostile
  • Rewrite so the inner loop walks contiguous memory
  • Keep the result identical (same copied data)
  • Explain the change in terms of cache lines and spatial locality

💡 Hint: Make the inner loop index the fastest-varying (contiguous) dimension of the layout.

Show solution
# SLOW: inner loop jumps a full row each step
def copy_slow(src, dst, N):
    for c in range(N):
        for r in range(N):
            dst[r*N + c] = src[r*N + c]

# FAST: inner loop walks consecutive addresses
def copy_fast(src, dst, N):
    for r in range(N):
        base = r*N
        for c in range(N):
            dst[base + c] = src[base + c]

N = 4
src = list(range(N*N)); dst = [0]*(N*N)
copy_fast(src, dst, N)
print(dst == src)   # True -- same result, cache-friendly order

Both produce identical output; only the memory access order differs. The fast version reads and writes consecutive addresses, so each 64-byte cache line loaded serves many iterations instead of one — the whole optimization is choosing the contiguous dimension as the inner loop.

Exercise 6 · Explain a real latency budgetIndustry scenario

Context: Senior engineers reason about performance with the memory hierarchy in their head — 'this is a cache-miss problem' vs 'this is a disk problem' changes the entire fix.

Your task: A service's p99 latency regressed. Using approximate hierarchy latencies, write the reasoning that distinguishes a cache/RAM issue from a disk/network issue and pick the right first move.

Requirements:

  • Lay out the order-of-magnitude latencies (L1 ~1ns, RAM ~100ns, SSD ~10µs+, network ~ms)
  • Explain how the magnitude of the regression points at a layer
  • Give the diagnostic you'd run for each hypothesis
  • State clearly these are approximate, machine-dependent orders of magnitude

💡 Hint: A jump from microseconds to milliseconds smells like disk or network, not cache; a modest constant-factor slowdown across the board smells like locality/cache.

Show solution
# Approximate orders of magnitude (machine-dependent):
#   L1 hit   ~1 ns        RAM     ~100 ns
#   L3 hit   ~10 ns       SSD     ~10-100 us
#   disk seek ~ms         network ~ms+
#
# Reasoning:
# * A 2-3x CPU-bound slowdown with no I/O change -> likely locality/cache
#   (data grew past L2/L3, or an access pattern regressed). Fix: profile,
#   check working-set size and loop/access order.
# * A jump from ~us to ~ms in p99 -> crossed into disk or network territory
#   (cache miss on a hot dataset, an added remote call, a slow query).
#   Fix: trace the request, look for new I/O, check cache hit-rate.
print('classify by the MAGNITUDE of the regression, then trace that layer')

The skill is mapping a symptom to a layer by its order of magnitude: nanosecond-to-microsecond effects are cache/RAM; microsecond-to-millisecond jumps are disk/network. These figures are approximate and vary by hardware, but the ratios are stable enough to guide the first diagnostic — which is what stops you optimizing the wrong layer.

© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in