Cache hit and cache miss
When the CPU needs data, it first looks in the cache. If found, all is well—but what if it's not found?
This lecture helps you understand the core mechanics of how caches work, and the key principle that makes caches effective—Principle of locality。
Everyday analogy: the desk in a library
You're writing a paper in the library. At most 4 books can fit on your desk—this is yourCache, its capacity is small but within arm's reach.
When you need a book, first check whether it's on the desk:
- On the table (cache hit): just open and use it, zero waiting.
- Not on the table (cache miss): get up and look on the bookshelf, bring it back and put it on the desk. If the desk is full, you have to put the book that hasn't been touched for the longest time back on the shelf.
This simple rule is exactly the essence of a computer cache system—"Keep the recently used, evict the rarely used"。
Going further, you will find: when writing a paper, you don't randomly flip through unrelated books. Over a certain period of time, you repeatedly consult the same set of reference materials. Of the books on those thousands of shelves in the library, the ones you actually touched may only be twenty or thirty. This isPrinciple of localityAt work.
Core concept: cache hit and miss
Basic Terms
| Term | English | Meaning |
|---|---|---|
| Cache hit | Cache Hit | The data requested by the CPU is found right in the cache and used directly |
| Cache miss | Cache Miss | The data requested by the CPU is not in the cache and must be loaded from the next (slower) level of storage |
| hit rate | Hit Rate | Number of hits / total accesses, the higher the better (ideally close to 100%) |
| Miss penalty | Miss Penalty | The extra time spent loading data from the next level after a miss |
| Hit time | Hit Time | Time required to read data when the cache hits |
A simple example
, arr
| Steps | Access address | Cache state (before access) | Result | Reason |
|---|---|---|---|---|
| 1 | 10 | [] | miss | Cache is empty, address 10 is not in it |
| 2 | 20 | [10] | miss | Address 20 is not in the cache |
| 3 | 10 | [10, 20] | hit | Address 10 is in the cache! (temporal locality) |
| 4 | 30 | [10, 20] | miss | Address 30 is not in the cache |
| 5 | 10 | [10, 20, 30] | hit | Address 10 hits again |
2 hits out of 5 accesses, hit rate = 40%. Not high, but it already demonstrates the principle of caching.
Why caches work: the principle of locality
If a program accesses memory completely randomly, the cache is almost useless—data is loaded in only to be of no use right away.
But real programs don't behave randomly. They exhibit strongLocality, and this is precisely the fundamental reason why caches are effective.
... They are contiguous in memory.
If a piece of data is accessed, it is likely to be accessed again in the near future.
Typical scenario:
- Variables in a Loop:
for i in range(1000000): total += i— VariableiandtotalAccessed 1 million times within a short period. - Function Hot Code: 20% of a program's code accounts for 80% of its execution time; those instructions get frequently re-accessed.
- Frequently called function parameters: Local variables in a recursive function pile up on the stack with each call.
Spatial Locality
If a data item is accessed, nearby data is also likely to be accessed.
Typical scenario:
- Sequential Array Traversal:
for i in range(len(arr)): sum += arr[i]—arr - Struct/object fields: access
student.nameAfter [that], it is very likely that the next access will be...student.age, they are adjacent in memory. - Sequentially Executed Instructions: After the CPU executes the instruction at address 100, the next instruction is likely at address 104.
Visualization of the two types of locality
时间局部性示意图:
时间轴: t1 t2 t3 t4 t5 t6 t7 t8
访问地址: A B A C A D B A
|_____|_____|_____|
地址 A 在短时间内被多次访问——时间局部性
空间局部性示意图:
内存地址: 100 104 108 112 116 120 124 128
访问顺序: *1 *2 *3 *4 *5
|_____________________________|
连续访问相邻地址——空间局部性
Modern CPUs exploit spatial locality to improve performance: when loading data from address 100 in memory, they also load addresses 100~164 (a full cache line, usually 64 bytes) into the cache. This way, when accessing address 104, it's already in the cache.
Cache replacement policy: what to do when it's full
Cache slots are limited; when the cache is full and new data needs to come in, it mustEvict an Old Data Item. The decision of "who gets evicted" is the cache replacement policy.
| Policy | English | Rules | evaluation |
|---|---|---|---|
| Least Recently Used | LRU (Least Recently Used) | Evict the data that has not been accessed for the longest time | Most commonly used, works very well |
| first-in, first-out | FIFO (First In First Out) | Evict the data that entered the cache earliest | Simple but average effectiveness |
| Least Frequently Used | LFU (Least Frequently Used) | Evict the data with the fewest accesses | Friendly to hot data, but old data may "refuse to leave" |
| Random replacement | Random | Randomly pick one to evict | Simplest to implement, works well |
LRU performs best in practice because it directly exploits temporal locality—"If it hasn't been used recently, it probably won't be used for a while."
Real CPU caches usually use approximate variants of LRU (such as pseudo-LRU) to reduce hardware implementation complexity.
Interactive demo: LRU cache simulator
The following Python program fully simulates the working process of an LRU cache. You can modify the access sequence and cache capacity to observe changes in the hit rate.
Example
LRU Cache Simulator (example demo)
Function:
1. Simulate the CPU's access sequence to memory addresses
2. Use LRU strategy to manage cache
3. Real-time label each access as 'hit' or 'miss'
4. Calculate and display hit rate
"""
from collections import OrderedDict
class LRUCache:
"""
LRU (Least Recently Used) Cache Simulator
Use OrderedDict to maintain access order; items closer to the end are more recently accessed.
"""
def __init__(self, capacity):
"""
Initialize cache
:param capacity: cache capacity (number of entries that can be stored)
"""
self.capacity = capacity
self.cache = OrderedDict() # Ordered dictionary: key=address, value=data
self.hits = 0 # Hit Count
self.misses = 0 # Miss Count
self.evictions = 0 # Eviction Count
def access(self, address):
"""
Simulate a memory access
:param address: the memory address to access
:return: (result_str, is_hit)
"""
if address in self.cache:
# Cache Hit!
self.hits += 1
# LRU policy: move the hit entry to the end (marked as most recently used)
self.cache.move_to_end(address)
return "HIT", True
else:
# Cache Miss
self.misses += 1
evicted = None
if len(self.cache) >= self.capacity:
# Cache is full, need to evict the least recently used (the first element of OrderedDict)
evicted_addr, _ = self.cache.popitem(last=False)
self.evictions += 1
evicted = evicted_addr
# Load new data from the 'next-level storage' (simulated as directly fetching data)
data = f"DATA_AT_{address}"
self.cache[address] = data
return "MISS", False
def hit_rate(self):
"""Calculate the current cache hit rate (percentage)"""
total = self.hits + self.misses
if total == 0:
return 0.0
return (self.hits / total) * 100
def current_state(self):
"""Returns current cached content (from oldest to newest)"""
return list(self.cache.keys())
def run_simulation(access_sequence, cache_capacity):
"""
Run a complete cache simulation
:param access_sequence: The memory access sequence to simulate (list of ints)
:param cache_capacity: cache capacity
"""
cache = LRUCache(cache_capacity)
print("=" * 70)
print(f"EXAMPLE LRU Cache Simulator")
print(fCache capacity: {cache_capacity} slots)
print(f"Access sequence: {access_sequence}")
print("=" * 70)
print(f"{'step':<6} {'address':<8} {'result':<8} {'cache status (old→new)':<40}")
print("-" * 70)
for i, addr in enumerate(access_sequence, 1):
result, is_hit = cache.access(addr)
state = cache.current_state()
hit_mark = "HIT [OK]" if is_hit else "MISS X"
print(f"{i:<6} {addr:<8} {hit_mark:<8} {str(state):<40}")
# Output Statistics
print("-" * 70)
print(f"\nStatistics results:")
print(f" Hit count (Hits) : {cache.hits}")
print(f" Miss Count (Misses): {cache.misses}")
print(f" Eviction count (Evictions): {cache.evictions}")
print(f" Hit Rate : {cache.hit_rate():.1f}%")
print()
return cache
# ============================================================
# Demo 1: Basic LRU behavior
# ============================================================
print("\n>>> Demo 1: Basic LRU Cache Behavior")
print(The access sequence shows temporal locality—address 10 is repeatedly accessed\n")
run_simulation(
access_sequence=[10, 20, 10, 30, 40, 10, 20, 50, 10, 20],
cache_capacity=4
)
# ============================================================
# Demo 2: Impact of Different Cache Capacities on Hit Rate
# ============================================================
print("\n>>> Demo 2: Effect of Cache Capacity on Hit Rate")
print("The performance of the same access sequence under different cache capacities\n")
test_sequence = [1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5,
1, 2, 3, 4, 5, 6, 7, 1, 2, 3, 4, 5, 6, 7]
print(f"Access sequence length: {len(test_sequence)}")
print(f"{'capacity':<8} {'hits':<8} {'misses':<8} {'hit rate':<10}")
print("-" * 35)
for cap in [1, 2, 3, 4, 5, 8, 16]:
c = LRUCache(cap)
for addr in test_sequence:
c.access(addr)
print(f"{cap:<8} {c.hits:<8} {c.misses:<8} {c.hit_rate():<8.1f}%")
# ============================================================
# Demo 3: Compare 'random access' vs 'locality access'
# ============================================================
print("\n>>> Demo 3: Locality access vs random access)
print("Same cache capacity (8), hit rate differences under different access patterns\n")
import random
random.seed(42)
# Pattern A: Access with locality (simulating loop traversal of an array)
local_access = []
for _ in range(20):
# Each loop, access consecutively in the 0-15 range, then jump
base = random.randint(0, 50)
for offset in range(5):
local_access.append(base + offset)
# Mode B: completely random access
random_access = [random.randint(0, 100) for _ in range(100)]
for label, seq in [("Locality Access", local_access), ("Random access", random_access)]:
c = LRUCache(8)
for addr in seq:
c.access(addr)
print(f" {label}: hit rate = {c.hit_rate():.1f}% (totalVisit {len(seq)} times)")
Interactive demo: real-time simulation of cache hit/miss
Randomly generate a memory access sequence with locality (30 accesses), animate each cache lookup process, track hit rate in real time and plot the change curve
Run this code and you will find:
- The larger the cache capacity, the higher the hit rate—but the improvement diminishes gradually (marginal effect)
- Access patterns with locality have much higher hit rates than random access—this is why caches are effective
- Under the LRU policy, repeatedly accessed addresses stay in the cache and are almost never evicted
Three types of cache misses
Not all "cache misses" are the same. Hardware engineers classify them into three types:
| Type | English | When it occurs | Can it be avoided? |
|---|---|---|---|
| Compulsory miss | Compulsory Miss (Cold Miss) | When a piece of data is accessed for the first time, it's not yet in the cache | Unavoidable (first access is always cold) |
| Capacity miss | Capacity Miss | The working set (data actively used by the program) is larger than the cache capacity | Increase the cache size or reduce the working set |
| Conflict miss | Conflict Miss | Different addresses are mapped to the same location in the cache (common in directly mapped caches) | Use set-associative or fully associative mapping |
For ordinary programmers, dealing with the first two types is more common:
- Compulsory miss: unavoidable, but the longer your program runs, the smaller its impact.
- Capacity miss: Can be mitigated by optimizing data structure size and access patterns. For example, use more compact data types, process large arrays in blocks.
Understanding cache misses is key to writing high-performance code. A simple rule of thumb: make your data access patterns cache-friendly—traverse arrays in memory layout order, avoid jumpy access, and keep data that's often used together close in memory.
Programming practice: cache-friendly code
Here's a classic example: when traversing a 2D array, row-major vs column-major traversal makes a world of difference in performance.
Example
Cache-friendly vs cache-unfriendly array traversal (Example demo)
Demonstrate how different access patterns to the same data affect cache performance
"""
import time
# Python list storage: two-dimensional array = list of lists
# In arr[i][j], arr[i] is an entire row, contiguous in memory.
# So row-wise traversal = cache-friendly, column-wise traversal = cache-unfriendly
def traverse_row_major(arr):
"""
Row-wise traversal: first fix row index i, then traverse columns j
Access pattern: arr[0][0], arr[0][1], arr[0][2]...
Memory layout: these elements are contiguous in memory -- good spatial locality!
"""
total = 0
rows = len(arr)
cols = len(arr[0])
for i in range(rows):
for j in range(cols):
total += arr[i][j]
return total
def traverse_column_major(arr):
"""
Column-wise traversal: first fix column index j, then traverse rows i
Access pattern: arr[0][0], arr[1][0], arr[2][0]...
Memory layout: these elements jump around in memory -- poor spatial locality!
"""
total = 0
rows = len(arr)
cols = len(arr[0])
for j in range(cols):
for i in range(rows):
total += arr[i][j]
return total
# Create a large array (1000 x 1000)
SIZE = 1000
print(f"Creating a {SIZE}x{SIZE} 2D array...")
arr = [[i * SIZE + j for j in range(SIZE)] for i in range(SIZE)]
# Pre-warm first to avoid cold start bias
traverse_row_major(arr)
traverse_column_major(arr)
# Formal test
print("\nStart performance comparison (run 3 times, take average):")
print("-" * 50)
def benchmark(func, arr, trials=3):
Average time over multiple runs
times = []
for _ in range(trials):
start = time.perf_counter()
func(arr)
elapsed = time.perf_counter() - start
times.append(elapsed)
return sum(times) / len(times)
row_time = benchmark(traverse_row_major, arr)
col_time = benchmark(traverse_column_major, arr)
print(f"Row-wise traversal (cache-friendly): {row_time:.4f} seconds")
print(fColumn-wise traversal (cache-unfriendly): {col_time:.4f} seconds)
print(fColumn-wise is slower than row-wise: {col_time / row_time:.1f} times)
print()
print("Cause analysis:")
print(When traversing by row, arr[i][0], arr[i][1], arr[i][2]... are stored contiguously in memory)
print(When accessing arr[i][0] for the first time, the CPU loads several subsequent elements into the cache together)
print(" Subsequent access to arr[i][1], arr[i][2] will directly hit the cache")
print()
print(" When traversing by column, arr[0][j], arr[1][j], arr[2][j]... are each separated by an entire row in memory.")
print(Almost every access requires loading from memory, leading to an extremely low cache hit rate)
# Supplement: Simple test - use Python's id() to check memory address
print("\nMemory layout verification (small array demo):")
small = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
print(fid of arr[0][0]: {id(small[0][0])})
print(f"arr[0][1] of id: {id(small[0][1])} (PoorValue: {id(small[0][1]) - id(small[0][0])})")
print(fid of arr[0][2]: {id(small[0][2])} (contiguous with [0][0]))
print(f"The id of arr[1][0]: {id(small[1][0])} (not contiguous with [0][2], because it spans a row)")