Course outline · 0% complete

0/28 lessons0%

Course overview →

The fetch-decode-execute cycle

lesson 1-2 · ~12 min · 2/28

How the CPU actually runs code

This loop is the ground floor of the whole course. Processes in unit 3, race conditions in unit 5, and the scheduler in unit 6 are all things done to this loop, and none of them can be reasoned about without it.

It is also why a stack trace or a debugger can say exactly where a program was, since the machine always knows precisely which instruction comes next.

The CPU does exactly one thing, forever, billions of times per second:

  1. Fetch the next instruction from RAM. The CPU keeps a counter called the program counter holding the address of that instruction.
  2. Decode it, working out what kind of instruction it is, whether an add, a copy, or a jump.
  3. Execute it, doing the work, usually updating a value in one of the tiny extremely fast storage slots inside the CPU called registers.

Then it returns to step 1. That is the whole trick, and every game, browser, and AI model is this loop running very simple instructions very fast.

Nothing in the cycle knows what a program means. There is no notion of a function, a loop, or an object at this level, only one instruction after another and a counter saying where to look next.

1. FETCHread from RAM2. DECODEwhat is it?3. EXECUTEdo the workrepeat, billions of times per second
The fetch-decode-execute cycle. The gold marker shows where the CPU is in the loop.

A CPU you can read

The simulator below is a fetch-decode-execute loop in fifteen lines of Python. Three pieces of state stand in for real hardware:

In the simulatorIn a real CPU
memory, a list of instruction stringsRAM holding encoded instructions
pc, an index into that listthe program counter register
acc, one running valuean accumulator register

Real instructions are numbers rather than text such as "ADD 3", and a real CPU has dozens of registers rather than one. Neither difference changes the shape of the loop.

The one detail worth watching is that pc advances before the instruction executes. That ordering is what lets a jump instruction work at all, since a jump is nothing more than an instruction that overwrites pc with a different address.

A CPU in fifteen lines

The program in memory loads 5, adds 3, adds 2, prints, and halts.

memory = ["LOAD 5", "ADD 3", "ADD 2", "PRINT", "HALT"]
acc = 0
pc = 0

while True:
    instruction = memory[pc]      # FETCH
    pc = pc + 1
    parts = instruction.split()   # DECODE
    op = parts[0]
    if op == "LOAD":              # EXECUTE
        acc = int(parts[1])
    elif op == "ADD":
        acc = acc + int(parts[1])
    elif op == "PRINT":
        print(acc)
    elif op == "HALT":
        break

Output

10

Traced by hand, acc starts at 0, LOAD 5 makes it 5, ADD 3 makes it 8, and ADD 2 makes it 10 before PRINT reports it.

The three comments mark the three phases, and each pass of the while loop is one complete cycle. HALT is the only instruction that ends the loop, which is why a real CPU with nothing to run executes an idle instruction rather than stopping.

Adding an instruction to the instruction set

A SUB branch subtracts its argument, and the program prints 6.

memory = ["LOAD 10", "SUB 4", "PRINT", "HALT"]
acc = 0
pc = 0

while True:
    instruction = memory[pc]
    pc = pc + 1
    parts = instruction.split()
    op = parts[0]
    if op == "LOAD":
        acc = int(parts[1])
    elif op == "ADD":
        acc = acc + int(parts[1])
    elif op == "SUB":
        acc = acc - int(parts[1])
    elif op == "PRINT":
        print(acc)
    elif op == "HALT":
        break

Output

6

The new branch is the ADD branch with one character changed, since subtraction differs from addition only in the operator applied to the accumulator.

Extending a real CPU is not this easy, and that is the interesting part. An instruction set is fixed in silicon, so adding one means a new chip generation, which is why compilers work so hard to express everything in terms of the instructions that already exist.

What the program counter holds

The variable pc holds the position in memory of the next instruction to fetch.

Fetch reads memory[pc] and then bumps pc forward, so it always points one step ahead of the instruction currently executing. A real CPU works the same way, and jump instructions work by simply overwriting pc.

elif op == "JUMP":
    pc = int(parts[1])   # the whole implementation of a loop

That one line is where every loop, if, and function call in every language ultimately lands. High-level control flow is compiled into conditional and unconditional writes to the program counter.

It is also why a crash can report an exact location. The counter is a real value the operating system can read out of the stopped process, which the next unit puts to work.