CPU performance
Clock speed, cores and cache, measured with models.
Do this lesson in the simulatorWhy does one computer feel fast and another slow? Three features of the CPU make the biggest difference: how fast its clock ticks, how many cores it has, and how much cache memory it holds. This lesson measures each one with models you can change, and asks where each helps and where it does not.
Clock speed
The clock sends pulses, and each pulse moves the fetch-execute cycle on. Clock speed is the number of pulses a second, measured in hertz: a 3.5 GHz processor ticks 3.5 billion times a second. Roughly, more ticks a second means more instructions carried out a second.
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
instructions = 12_000_000_000 # a heavy job: twelve billion instructions
for ghz in [1.0, 2.0, 3.5]:
per_second = ghz * 1_000_000_000 # roughly one instruction per tick
print(f"{ghz} GHz: {instructions / per_second:.1f} seconds")
Double the clock speed and the job takes half the time. But a faster clock makes the processor hotter and uses more power, which matters a great deal in a battery-powered robot.
Cores
A core is a complete processing unit. A dual-core processor can fetch and execute two instructions at the same moment, a quad-core four. More cores only help when the work can be split into parts that do not depend on each other:
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
def job_time(total_seconds, cores, can_split):
"""Time for a job on `cores` cores, when `can_split` of it (0 to 1) can run in parallel."""
parallel = total_seconds * can_split / cores
serial = total_seconds * (1 - can_split)
return serial + parallel
for split in [1.0, 0.5, 0.0]:
times = [round(job_time(60, cores, split), 1) for cores in [1, 2, 4, 8]]
print(f"{int(split * 100)}% can be split: 1, 2, 4, 8 cores ->", times, "seconds")
A job that is all one long chain of steps, where each step needs the answer from the last, gets no faster at all. Processing the robot's camera image, where every pixel can be worked on separately, gets much faster. Doubling the cores rarely halves the time in practice.
Cache
Getting data from main memory is slow compared with the CPU's speed. Cache is a small, very fast memory in or next to the CPU that keeps copies of the data used most recently. When the CPU needs something already in the cache, a hit, it is fast; when it is not, a miss, it must go to slower main memory.
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
def run_cache(requests, size):
"""Count hits and misses with a cache holding the `size` most recently used addresses."""
cache = []
hits = 0
for address in requests:
if address in cache:
hits = hits + 1
cache.remove(address)
elif len(cache) == size:
cache.pop(0) # forget the least recently used
cache.append(address)
return hits, len(requests) - hits
requests = [1, 2, 3, 1, 2, 3, 4, 1, 2, 3, 5, 1, 2, 3]
for size in [1, 2, 3, 4]:
hits, misses = run_cache(requests, size)
print(f"cache of {size}: {hits} hits, {misses} misses")
A cache too small for the pattern gets no hits at all. Once it can hold the addresses the program keeps coming back to, most requests become hits and the program runs faster. But cache memory is expensive, so processors have only a little of it. Programs usually reuse the same few pieces of data again and again, which is why a modest cache helps so much.
Putting it together
| Feature | More of it means | Limits |
|---|---|---|
| Clock speed | more instructions each second | heat, power use |
| Cores | several instructions at once | only helps work that can be split |
| Cache | fewer slow trips to main memory | expensive; helps less once it is big enough |
Task: the cache model
Use run_cache(requests, size) on the request list [5, 6, 5, 7, 5, 6, 8, 5, 6, 7], for cache sizes 1 to 6. Print one line for each size in the form size 2: 4 hits, then print best size: <n> for the smallest cache that gets the most hits, working it out with a loop.
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
def run_cache(requests, size):
cache = []
hits = 0
for address in requests:
if address in cache:
hits = hits + 1
cache.remove(address)
elif len(cache) == size:
cache.pop(0)
cache.append(address)
return hits, len(requests) - hits
requests = [5, 6, 5, 7, 5, 6, 8, 5, 6, 7]
Challenges
- With 80% of a job splittable, how many cores does it take to get under 20 seconds for a 60-second job?
- Make a request list where a cache of 2 gets no hits at all.
- Why does a phone slow its processor down when it gets hot?