- Explain the parts of a von Neumann computer and the fetch–decode–execute cycle
- Compute CPU time and average memory access time
- Write integers in two's complement and recognise overflow
- Encode a number in IEEE 754 and explain rounding errors
The 3 GHz processor in your phone ticks 3 billion times a second, yet if you type 0.1 + 0.2 == 0.3 in Python the answer is False. Both facts follow from how a computer is built: the processor fetches and executes instructions one by one, and it stores numbers in a finite number of bits. In this lesson we study the parts of the hardware that affect programmers the most.
The von Neumann architecture and the instruction cycle
Program and data are kept in the same memory; the processor (arithmetic logic unit, control unit and registers) fetches instructions from memory one after another and executes them; everything is connected by a bus. To change the program you change the memory contents, not the wiring.
| Register | Role |
|---|---|
| PC — program counter | address of the next instruction |
| IR — instruction register | the instruction being executed |
| MAR / MDR | memory address and the data read from or written to it |
| ACC and general registers | intermediate results of calculations |
| Flags | zero, sign, carry and overflow bits |
- 1Fetch
The address in PC goes to MAR, the instruction is read from memory into IR, and PC moves to the next instruction.
- 2Decode
The control unit works out the operation code and the operands.
- 3Execute
The ALU computes, or data is read from or written to memory; a jump instruction changes PC. Then the cycle repeats.
memory = [('LOAD', 5), ('ADD', 6), ('STORE', 7), ('PRINT', 7), ('HALT', 0), 20, 22, 0]
pc, acc, executed = 0, 0, 0
while True:
op, addr = memory[pc]
pc += 1
executed += 1
if op == 'LOAD':
acc = memory[addr]
elif op == 'ADD':
acc += memory[addr]
elif op == 'STORE':
memory[addr] = acc
elif op == 'PRINT':
print('memory[7] =', memory[addr])
elif op == 'HALT':
break
print('instructions executed:', executed, '| PC =', pc, '| ACC =', acc)▸ Expected output
memory[7] = 42 instructions executed: 5 | PC = 5 | ACC = 42
memory[pc]), increment PC, decode (if op == …), execute.- tCPU time of the program, s
- Nnumber of executed instructions
- CPIaverage clock cycles per instruction
- fclock frequency, Hz
The “iron law” of processor performance: to go faster you need fewer instructions (a better algorithm and compiler), a lower CPI (pipelining, caches) or a higher clock rate.
A program executes 2 · 10⁹ instructions with CPI = 1.5 at 3 GHz. What is its CPU time? What if the compiler cuts the instruction count by 20% but CPI rises to 1.6?
Show solutionHide solution
New version: N = 1.6 · 10⁹, t = 1.6 · 10⁹ · 1.6 / (3 · 10⁹) = 2.56 / 3 ≈ 0.853 s.
Even though each instruction takes more cycles, the total time fell by ≈ 15% — never judge by one metric alone.
The memory hierarchy and caches
The processor is roughly a hundred times faster than main memory (RAM) — the von Neumann bottleneck. The fix is a memory hierarchy: small, fast cache levels close to the processor and large, slow memories further away. Caches rely on locality: data used recently is likely to be used again soon (temporal locality), and so are neighbouring addresses (spatial locality). That is why memory is fetched in 64-byte cache lines.
| Level | Typical size | Approximate latency |
|---|---|---|
| Registers | a few hundred bytes | ≈ 1 cycle |
| L1 cache | 32–64 KB per core | ≈ 1 ns |
| L2 cache | 0.25–2 MB per core | ≈ 3–5 ns |
| L3 cache | shared, tens of MB | ≈ 10–20 ns |
| Main memory (RAM) | gigabytes | ≈ 60–100 ns |
| SSD | hundreds of GB – TB | ≈ 20–100 µs |
- AMATaverage memory access time
- thitaccess time on a cache hit
- mmiss rate
- tmissmiss penalty (going to the next level)
L1: 1 ns, miss rate 5%. L2: 4 ns, misses 20% of the requests that reach it. RAM: 100 ns. Find the AMAT. What would it be without L2?
Show solutionHide solution
AMAT = 1 + 0.05 · 24 = 1 + 1.2 = 2.2 ns.
Without L2: AMAT = 1 + 0.05 · 100 = 6 ns — about 2.7 times slower.
Takeaway: cache-friendly code (walking an array in order) is much faster than scattered accesses (a linked list).
Integers: two's complement
In n-bit two's complement the top bit has a negative weight: −2ⁿ⁻¹. So negative numbers start with 1, and no separate “subtractor” is needed: the ALU computes a − b as a + (−b). Addition itself is built from logic gates: in a half adder the sum bit is a XOR b and the carry is a AND b.
- NOT xall bits of x inverted
- nnumber of bits (8 bits: −128 … 127)
a) Write −45 in 8-bit two's complement. b) What does 100 + 50 give in 8-bit signed arithmetic?
Show solutionHide solution
Check: −128 + 64 + 16 + 2 + 1 = −45 ✓ (read as unsigned it is 211 = 256 − 45).
b) 100 = 01100100, 50 = 00110010, sum = 10010110.
The top bit became 1 → −128 + 16 + 4 + 2 = −106. Two positive numbers gave a negative sum — that is overflow: 150 > 127. The result wraps around: 150 − 256 = −106.
Floating point: IEEE 754
- ssign bit (1 bit)
- ebiased exponent (8 bits, bias 127)
- mfraction of the mantissa (23 bits; the leading 1 is implicit)
The 32-bit (single) format. The 64-bit double: 1 + 11 + 52 bits, bias 1023, about 15–16 decimal digits of precision. Python's float is a double.
Write −6.25 in IEEE 754 single precision (bits and hex code).
Show solutionHide solution
6.25 = 6 + 0.25 = 110₂ + 0.01₂ = 110.01₂ = 1.1001₂ · 2².
Exponent: e = 2 + 127 = 129 = 10000001₂.
Mantissa (without the leading 1): 1001 followed by 19 zeros.
Bits: 1 | 10000001 | 10010000000000000000000
Groups of 4: 1100 0000 1100 1000 0000 … → C0C80000₁₆.
import struct, math
def to_bits(x, bits=8):
return format(x % 2 ** bits, '0' + str(bits) + 'b')
def from_bits(s):
v = int(s, 2)
return v - 2 ** len(s) if s[0] == '1' else v
print(to_bits(45), to_bits(-45), from_bits('11010011'))
print('100 + 50 in 8 bits:', from_bits(to_bits(100 + 50)))
raw = struct.pack('>f', -6.25).hex()
print('-6.25 as float32:', raw, format(int(raw, 16), '032b'))
print(0.1 + 0.2, 0.1 + 0.2 == 0.3, math.isclose(0.1 + 0.2, 0.3))
print(struct.unpack('>f', struct.pack('>f', 0.1))[0])▸ Expected output
00101101 11010011 -45 100 + 50 in 8 bits: -106 -6.25 as float32: c0c80000 11000000110010000000000000000000 0.30000000000000004 False True 0.10000000149011612
x % 2 ** bits wraps the number onto the n-bit “circle” — that is two's complement itself. struct shows the real 32 bits: the same C0C80000 we got by hand. 0.1 is an infinite binary fraction (0.000110011…₂), so in float32 it becomes 0.10000000149…, and in double 0.1 + 0.2 = 0.30000000000000004.Key points
- In a von Neumann computer program and data share memory; the CPU repeats fetch–decode–execute.
- CPU time is t = N · CPI / f; performance depends on all three factors.
- Caches rely on locality; AMAT = thit + m · tmiss.
- Two's complement: −x = NOT x + 1, n bits cover −2ⁿ⁻¹ … 2ⁿ⁻¹ − 1; leaving the range is overflow.
- IEEE 754: sign, biased exponent, mantissa; numbers like 0.1 are not exact, so never compare floats with ==.
Check yourself
10 questions. Every correct answer earns XP.