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

TermEnglishMeaning
Cache hitCache HitThe data requested by the CPU is found right in the cache and used directly
Cache missCache MissThe data requested by the CPU is not in the cache and must be loaded from the next (slower) level of storage
hit rateHit RateNumber of hits / total accesses, the higher the better (ideally close to 100%)
Miss penaltyMiss PenaltyThe extra time spent loading data from the next level after a miss
Hit timeHit TimeTime required to read data when the cache hits

A simple example

, arr

StepsAccess addressCache state (before access)ResultReason
110[]missCache is empty, address 10 is not in it
220[10]missAddress 20 is not in the cache
310[10, 20]hitAddress 10 is in the cache! (temporal locality)
430[10, 20]missAddress 30 is not in the cache
510[10, 20, 30]hitAddress 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: accessstudent.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.

PolicyEnglishRulesevaluation
Least Recently UsedLRU (Least Recently Used)Evict the data that has not been accessed for the longest timeMost commonly used, works very well
first-in, first-outFIFO (First In First Out)Evict the data that entered the cache earliestSimple but average effectiveness
Least Frequently UsedLFU (Least Frequently Used)Evict the data with the fewest accessesFriendly to hot data, but old data may "refuse to leave"
Random replacementRandomRandomly pick one to evictSimplest 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)")
example cache hit/miss real-time simulator — animated demo + Chart.js hit rate curve

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

Left: simulation area
Access sequence preview dots
Current access address
Current access address:--
Hit/Miss Result Indicator
Ready
4 cache slot grid
Slot 1Empty
Slot 2Empty
Slot 3Empty
Slot 4Empty
Control button
Right side: Statistics + Charts
Hit/Miss/Hit Rate Statistics
Hit count0
Number of misses0
hit rate--
Chart.js hit rate variation 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:

TypeEnglishWhen it occursCan it be avoided?
Compulsory missCompulsory Miss (Cold Miss)When a piece of data is accessed for the first time, it's not yet in the cacheUnavoidable (first access is always cold)
Capacity missCapacity MissThe working set (data actively used by the program) is larger than the cache capacityIncrease the cache size or reduce the working set
Conflict missConflict MissDifferent 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)")
other extensions