Cache Replacement Policies

A processor's cache has room for only a handful of blocks per set. A running program touches far more addresses than that. Every time a new block must be brought in and the set is already full, the cache controller faces a small but consequential decision. Which resident block gets thrown out? Guess well, and the next few accesses hit fast. Guess poorly, and the program stalls on memory again almost immediately. This single decision, repeated billions of times per second, is the job of a cache replacement policy. The stakes are real. Modern out-of-order cores can execute several instructions per cycle. But they stall for hundreds of cycles on a last-level cache miss that goes to DRAM. A replacement policy that is even a few percentage points worse at predicting future reuse matters. It translates directly into measurable slowdown across an entire workload. This holds from web servers to video games to machine learning inference. Because the decision must be made in hardware, on the critical path of nearly every memory access, the policy must also be cheap. It needs only a few bits of state per block, evaluated in at most a cycle or two. This article builds on Set-Associative Caches and the idea, familiar from Fully Associative Caches and the Design Spectrum, that when a set has multiple ways, there is a genuine choice of victim. We work through the three classical policies — Least Recently Used (LRU), First-In-First-Out (FIFO), and Random. We explain exactly how each is implemented, what it costs in hardware, and how well it approximates the theoretical optimum. We derive complexity honestly (state bits, comparator cost, update latency). We also give a full worked trace so the differences are not abstract. By the end, you should be able to implement true LRU and its cheaper approximations from scratch. You should be able to explain why LRU is not always better than random. You should also see how the reuse patterns this article studies connect directly to Cache Misses: The Three Cs and to the write-back bookkeeping described in Cache Write Policies.

Key takeaways
  • A replacement policy picks which cached block to evict when a new block must be installed into a full set.
  • LRU evicts the block unused for the longest time; it needs recency-ordering state per set, expensive beyond 4–8 ways.
  • FIFO evicts the oldest-installed block regardless of recent use; needs only an insertion-order counter, much cheaper than LRU.
  • Random evicts an arbitrary way using a cheap pseudo-random source; near-zero state, and surprisingly competitive at high associativity.
  • Real hardware almost never implements true LRU beyond small associativity; it uses approximations like pseudo-LRU (tree bits) or clock/NRU.
  • The right policy is a hardware-cost-versus-hit-rate tradeoff, not a search for one universally "best" answer.
The core insight

A replacement policy trades hardware state and update cost for a prediction of which cached block is least likely to be reused soon.

The idea

A cache is organized into sets, each holding a fixed number of ways (blocks) — this is exactly the associativity discussed for Set-Associative Caches. On a miss, the requested block must be loaded into some way of its set. If every way in that set already holds valid data, one resident block must be evicted to make room. The rule that chooses the victim is the replacement policy. The theoretical ideal is Belady's optimal algorithm (MIN): evict the block whose next reference lies furthest in the future, or is never referenced again. Belady's algorithm provably minimizes the number of misses for a given cache size and associativity, but it requires knowing the future reference stream — impossible in real hardware operating on a running program. Every practical policy is therefore a heuristic that tries to approximate MIN using only information about the past: recency of use, order of insertion, or nothing at all. The three canonical heuristics differ in what past information they track and how much it costs to maintain:

  • LRU (Least Recently Used) assumes that a block not accessed in a long time is unlikely to be accessed again soon — an appeal to temporal locality. It requires ordering all ways in a set by time-of-last-use.
  • FIFO (First-In-First-Out) assumes only that older residents are more expendable than newer ones, ignoring whether a block was used after being installed. It requires only an insertion timestamp or a rotating pointer.
  • Random makes no assumption at all; it picks a way uniformly (or pseudo-uniformly) at random. It requires essentially no per-set state beyond a global pseudo-random number source.

The key design tension is this: the more information a policy uses about access history, the better it can approximate MIN — but the more state and update logic it requires per set, on every access, not just every miss. LRU must be updated on every hit (to keep recency order current), which is what makes it expensive; FIFO and Random only need bookkeeping on eviction, which is much cheaper.

How it works

True LRU conceptually keeps, for each set, a full ordering of the n ways from most-recently-used to least-recently-used. On every access (hit or fill) to way w, w moves to the "most recently used" end; on a miss requiring eviction, the way at the "least recently used" end is chosen. There are two standard hardware realizations: 1. Counter-based LRU. Each way has a ⌈log⁡2n⌉\lceil \log_2 n \rceil-bit counter recording its rank. On an access to way ww with old counter value cwc_w: every way whose counter is less than cwc_w is incremented by 1, and ww's counter is set to 0 (most recent). The way with counter value n−1n-1 is the LRU victim. 2. Matrix (pairwise) LRU. Maintain an n×nn \times n bit matrix MM where M[i][j]=1M[i][j] = 1 means "way ii was used more recently than way jj." On an access to way ii: set row ii to all 1s and column ii to all 0s. The LRU way is the one whose row is all 0s (never more recent than anything still valid). Both realizations are exact LRU but scale as O(n2)O(n^2) bits or O(n)O(n) comparator work per access, which is why true LRU is common only for 2-way to 8-way caches. Beyond that, hardware uses approximations:

  • Pseudo-LRU (PLRU), tree-based. Use a binary tree of n−1n-1 bits over nn ways. Each bit indicates which half of the subtree was not most recently used. On access, flip the bits along the path to point away from the accessed way. On eviction, walk the tree following the "not recently used" bits to a leaf. This costs only n−1n - 1 bits and O(log⁡n)O(\log n) update work — dramatically cheaper than exact LRU's O(n2)O(n^2), at the cost of occasionally evicting a not-quite-LRU block.
  • NRU / Clock (1-bit approximation). Each way has one "referenced" bit, set on access. On eviction, scan (or use a rotating pointer) for a way with bit = 0, clearing bits to 0 as you pass over ways with bit = 1 (like the OS clock/second-chance page algorithm). This is the cheapest useful approximation: 1 bit per way.

FIFO needs only a small per-set pointer (or per-way insertion-order field) recording which way was filled longest ago; it advances on fills only, never on hits, so it is far cheaper to update than LRU.

Random needs a pseudo-random number generator (often a small linear-feedback shift register, LFSR, shared across many sets) to pick a way index modulo the associativity; no per-set state at all is required. ``` // Generic replacement-policy interface, invoked on every memory access function ACCESS(set, tag): way = FIND_WAY(set, tag) // -1 if miss if way != -1: HIT_UPDATE(set, way) // policy-specific bookkeeping on a hit return HIT else: victim = CHOOSE_VICTIM(set) // policy-specific victim selection INSTALL(set, victim, tag) // evict victim, load new block FILL_UPDATE(set, victim) // policy-specific bookkeeping on a fill return MISS // --- LRU (counter-based), n ways per set --- function HIT_UPDATE_LRU(set, way): old = counter[set][way] for w in 0..n-1: if counter[set][w] < old: counter[set][w] += 1 counter[set][way] = 0 // mark most recently used function CHOOSE_VICTIM_LRU(set): return argmax_w counter[set][w] // counter == n-1 identifies LRU way function FILL_UPDATE_LRU(set, way): HIT_UPDATE_LRU(set, way) // installing counts as "just used" // --- FIFO --- function CHOOSE_VICTIM_FIFO(set): return ptr[set] // pointer marks oldest-filled way function FILL_UPDATE_FIFO(set, way): ptr[set] = (ptr[set] + 1) mod n // advance only on a fill, never on a hit function HIT_UPDATE_FIFO(set, way): return // no-op: FIFO ignores hits entirely // --- Random --- function CHOOSE_VICTIM_RANDOM(set): return LFSR_NEXT() mod n // cheap pseudo-random index function HIT_UPDATE_RANDOM(set, way): return // no state to maintain at all // --- Pseudo-LRU (binary-tree approximation), n = 2^k ways --- function HIT_UPDATE_PLRU(set, way): node = root(set) for bit in path_bits(way, k): // bits of way's index, MSB first setDirection(node, bit) // point tree bit AWAY from this half node = child(node, bit) function CHOOSE_VICTIM_PLRU(set): node = root(set) for level in 1..k: bit = treeBit(node) // follow "not recently used" direction node = child(node, bit) return leafToWay(node) ``` The figure below shows a 4-way set under LRU, FIFO, and Random side by side, so the different bookkeeping each policy needs is visible directly.

Figure
Three replacement policies managing the same 4-way set
Set with 4 ways — state kept by each policy LRU (counters, rank 0 = newest) Way A rank=2 Way B rank=0 (MRU) Way C rank=1 Way D rank=3 (LRU→evict) updates on every HIT cost: O(n) compares/access FIFO (insertion order pointer) Way A filled t=1 Way B filled t=3 Way C filled t=2 Way D filled t=0 (oldest→evict) updates on FILL only cost: O(1) pointer/set Random (LFSR index) Way A Way B Way C ← LFSR picked Way D no per-set state cost: O(1), shared LFSR All three must pick a victim among {A,B,C,D} on the next miss to this set. LRU evicts D (least recently used), FIFO evicts D (oldest), Random evicted C by chance.

Complexity & correctness

State cost per set (associativity nn):

  • Exact LRU (counter form): n⋅⌈log⁡2n⌉n \cdot \lceil \log_2 n \rceil bits, e.g. 8 bits for 4 ways, 24 bits for 8 ways.
  • Exact LRU (matrix form): (n2)\binom{n}{2} bits, e.g. 6 bits for 4 ways, 28 bits for 8 ways — grows as O(n2)O(n^2).
  • Pseudo-LRU (tree): n−1n-1 bits, e.g. 3 bits for 4 ways, 7 bits for 8 ways — linear in nn.
  • FIFO: ⌈log⁡2n⌉\lceil \log_2 n \rceil bits per set (one pointer), independent of how many bits track which way beyond identifying the oldest.
  • Random: effectively 0 bits per set; a global LFSR is amortized across the whole cache.

Update time per access:

  • Exact LRU (counter form) must compare and possibly increment all nn counters on every hit and every fill: O(n)O(n) work per access, all of it on the critical path if the update must complete before the next access to that set.
  • Exact LRU (matrix form) touches a row and column: O(n)O(n) bit writes, but can be done with wide combinational logic in parallel, effectively O(1)O(1) latency for small nn at the cost of O(n2)O(n^2) area.
  • Pseudo-LRU touches log⁡2n\log_2 n tree bits per access: O(log⁡n)O(\log n) time and space, which is why it dominates practical designs above 4–8 ways (e.g., many 8-way and 16-way last-level caches use tree-PLRU).
  • FIFO and Random touch O(1)O(1) state per access (FIFO only on fills; Random needs no update at all beyond advancing the shared LFSR).

Why LRU tends to work well. LRU is a direct hardware embodiment of the stack property: for a sequence of accesses, if you sort the working set by recency, the probability that the next access falls within the kk most-recently-used items is high whenever the program exhibits temporal locality (loops, repeated calls, hot data structures). Formally, LRU is optimal within the class of "stack" replacement algorithms under Belady's stack-property framework, meaning that a cache of size kk under LRU is guaranteed to contain a superset of the blocks a cache of size k′<kk' < k would contain, for the same reference string. This guarantees LRU is free of Belady's anomaly — increasing cache size can never increase the miss count under LRU, unlike under FIFO, where it famously can.

Why LRU can still lose to Random or FIFO. LRU's guarantee applies to reference strings whose future resembles their past. Certain access patterns are adversarial to recency-based reasoning: a sequential scan larger than the cache (e.g., iterating over an array bigger than the whole cache) causes LRU to evict exactly the block that will be needed next, achieving essentially a 0% hit rate, while Random, by chance, retains some useful blocks. This is the classic weakness exploited by scan-resistant designs. More subtly, at high associativity (e.g., 16-way) the marginal benefit of exact recency tracking over a good pseudo-random or pseudo-LRU choice shrinks, because with many ways the "wrong" eviction rarely removes a block about to be reused — empirically, Random and PLRU trail true LRU by well under 1% miss rate at 8-way and above, while costing a small fraction of the hardware.

FIFO's correctness caveat — Belady's anomaly. Unlike LRU, FIFO does not obey the stack property. There exist reference strings for which a larger FIFO cache produces more misses than a smaller one — Belady's original counterexample used exactly this policy. This means FIFO's behavior is not monotünically well-behaved as capacity grows, a real theoretical wart, even though in practice its average-case behavior on typical workloads is only modestly worse than LRU.

Random's correctness argument. Random offers no worst-case guarantee at all — it can, with vanishingly small probability, evict the most-reused block repeatedly — but its expected behavior can be analyzed: for a uniformly random victim among nn ways, each resident block survives an eviction round with probability 1−1/n1 - 1/n, giving an expected residency (in rounds) of nn, independent of access history. This history-independence is exactly what makes Random immune to adversarial and scan-heavy patterns that defeat LRU, at the price of giving up the ability to exploit genuine temporal locality when it is present.

Bottom line trade-off table (informal):

PolicyState/setUpdate costMiss-rate qualityScan-resistant?Belady's anomaly?
Exact LRUO(n2)O(n^2) or O(nlog⁡n)O(n\log n) bitsO(n)O(n) every accessBest among simple heuristicsNoImmune (stack algorithm)
Pseudo-LRUO(n)O(n) bitsO(log⁡n)O(\log n) every accessClose to exact LRUNoImmune (stack-like)
FIFOO(log⁡n)O(\log n) bitsO(1)O(1) on fill onlyNoticeably worse on loopsNoCan occur
Random≈0\approx 0 bitsO(1)O(1), fill onlyWorse on locality, better on scansYesImmune (no monotonic structure to violate)

Worked example

Consider a 2-way set-associative cache set with capacity for 2 blocks, and the reference stream of block addresses:

A, B, A, C, A, B, DA,\ B,\ A,\ C,\ A,\ B,\ D

We trace LRU, FIFO, and Random (assume Random happens to always evict the way holding the block that was installed first, to make the trace concrete and comparable — call this "Random-instance").

StepRefLRU state beforeLRU resultFIFO state beforeFIFO resultRandom-instance beforeRandom result
1A{ }miss, load A{ }miss, load A{ }miss, load A
2B{A}miss, load B{A}miss, load B{A}miss, load B
3A{A(old),B(new)}hit on A, A becomes MRU{A(oldest),B}hit on A (FIFO ignores hits){A,B}hit on A
4C{B(old),A(new)}miss, evict B (LRU), load C → {A,C}{A(oldest),B}miss, evict A (oldest), load C → {B,C}{A,B}miss, evict A (first-installed), load C → {B,C}
5A{C(old),A(new... wait A evicted?)}miss (A was evicted at step 4!), evict C, load A → {A,C}...{B(oldest),C}miss, evict B, load A → {C,A}{B,C}miss, evict B, load A → {C,A}
6B{C, A(new)}miss, evict C, load B → {A,B}{C(oldest),A}miss, evict C, load B → {A,B}{C,A}miss, evict C, load B → {A,B}
7D{A(old),B(new)}miss, evict A, load D → {B,D}{A(oldest),B}miss, evict A, load D → {B,D}{A,B}miss, evict A, load D → {B,D}

Reading the table carefully: all three policies produce the same eviction sequence on this particular short trace, and all get exactly one hit (step 3) out of seven references. This is intentional — it shows that for short or simple traces the policies frequently agree, and differences only emerge over longer traces or working sets that exceed capacity. Let's extend the trace to expose a real divergence, appending a repeating loop over three blocks in a 2-way set — a classic case where LRU is worse than Random:

…, E, F, G, E, F, G, E, F, G\ldots,\ E,\ F,\ G,\ E,\ F,\ G,\ E,\ F,\ G

With only 2 ways and 3 distinct blocks accessed cyclically, LRU evicts exactly the block needed next on every single reference after the set fills — a 0% steady-state hit rate, because whichever block was used least recently is, by the cyclic pattern, the very next one requested. FIFO behaves identically here (it also evicts in a fixed rotating order). Random, however, occasionally happens to keep the right block by chance, giving a small but nonzero expected hit rate above 0% — a concrete demonstration that recency-based reasoning is not universally superior, and that Random's history-independence has a genuine, provable use case (the classic "cyclic reference string longer than cache" pathology).

Common pitfalls & traps

PitfallWhy it happensFix
Treating FIFO as "LRU without the hardware"FIFO ignores hits entirely, so a block hit-heavy but old still gets evictedRecognize FIFO tracks insertion order, not usage; expect worse hit rate on loops
Assuming bigger cache always means fewer missesTrue for LRU (stack property) but false for FIFO (Belady's anomaly)Only rely on monotonic capacity behavior for stack algorithms like LRU/PLRU
Implementing "LRU" as just a 1-bit recently-used flagA single bit cannot express a total order among ≥3 waysUse NRU/clock deliberately as an approximation, not a claim of true LRU
Updating LRU counters only on missesHits must update recency too, or "LRU" becomes closer to FIFOEnsure HIT_UPDATE mutates state exactly like FILL_UPDATE for LRU
Scaling exact LRU to high associativityCounter/matrix LRU costs O(n2)O(n^2) bits, silently exploding area at 16+ waysSwitch to tree pseudo-LRU or NRU beyond ~8 ways
Believing Random has no legitimate use caseRandom looks "dumb" compared to LRU on typical workloadsRemember Random is scan-resistant and immune to adversarial cyclic patterns that defeat LRU

Pioneers & history

Origins · 1 minLászló A. Bélády and how this was discovered
LA
László A. Bélády
1928–2021 · IBM Research, working on virtual memory and paging systems in the 1960s

Bélády's 1966 paper on page-replacement algorithms introduced the MIN (optimal, clairvoyant) algorithm as a yardstick, formalized the stack property that makes LRU immune to anomalies, and — with Belady, Nelson, and Shedler in 1969 — constructed the explicit reference string demonstrating that FIFO can suffer more page faults on a larger memory, the result now called Belady's anomaly.

History
1966
— Bélády formalizes optimal (MIN) replacement and analyzes practical heuristics for virtual memory paging.
1968
— Denning's working-set model gives a theoretical grounding for why recency-based heuristics like LRU track locality well.
1969
— Belady, Nelson, and Shedler publish the reference string proving FIFO can violate cache-size monotonicity (Belady's anomaly).
1970
s — Clock (second-chance) and NRU approximations are adopted in operating-system page replacement as cheap stand-ins for LRU.
1990
s–2000s — Tree-based pseudo-LRU becomes standard in commercial set-associative processor caches as associativity grows beyond 4–8 ways.

Where this matters

Every processor with a set-associative cache — L1, L2, and last-level caches in essentially all commercial CPUs and many GPUs — implements some version of these policies at every level of the Set-Associative Caches hierarchy. Real designs almost always choose an approximation: many commercial 8-way and 16-way last-level caches use tree-based pseudo-LRU or NRU-style bits rather than exact LRU, because exact LRU's O(n2)O(n^2) state and update logic become prohibitive at high associativity. Translation Lookaside Buffers (TLBs), themselves small associative caches for address translations, use the same family of policies at a much smaller scale. Operating-system page replacement for virtual memory is the software-side twin of this problem, using clock/NRU algorithms directly descended from the hardware ideas discussed here. Beyond CPUs, content delivery networks, database buffer pools, and key-value stores such as web caches all implement software analogs of LRU, FIFO, or randomized eviction, facing exactly the same state-versus-accuracy tradeoff at a much coarser granularity. Understanding replacement policy behavior is also the natural bridge to Cache Misses: The Three Cs, since a poor replacement policy manifests specifically as extra conflict misses, and to Cache Write Policies, where the choice of victim interacts with whether dirty data must be written back before eviction.

Ask the tutor: How would the choice of replacement policy change if the cache were fully associative instead of set-associative?

Why don't real processors just implement true LRU everywhere?

True LRU requires tracking a total recency order among all ways in a set, which costs O(n2)O(n^2) bits in the matrix formulation (or O(nlog⁡n)O(n \log n) with per-way counters) and must be updated on every single hit, not just every miss. For 2-way or 4-way caches this is cheap and common. But for 8-way, 16-way, or higher associativity — typical of modern last-level caches — the area and timing cost of exact LRU update logic becomes significant relative to the modest hit-rate gain it provides over cheaper approximations. Designers instead use tree-based pseudo-LRU (needing only n−1n-1 bits and O(log⁡n)O(\log n) update work) or simple NRU/clock schemes with 1 bit per way, both of which get within a fraction of a percent of true LRU's hit rate in practice.

Is Random replacement ever actually a good engineering choice?

Yes, in specific circumstances. Random needs essentially zero per-set state — just a shared pseudo-random source like an LFSR — making it attractive when die area or design complexity is at a premium. More importantly, Random is provably immune to the worst-case pathology that defeats LRU: a sequential or cyclic scan larger than the cache, which causes LRU to evict precisely the block needed next on every access. Because Random's eviction choice doesn't depend on history, it cannot be driven to a systematic 0% hit rate by an adversarial access pattern the way LRU can. Some real designs deliberately mix in randomness or use "not-recently-used" schemes partly for this scan-resistance property.

What exactly is Belady's anomaly, and does it affect LRU?

Belady's anomaly is the surprising fact that, for some reference strings and some replacement policies, increasing the cache's capacity can increase the number of misses rather than decrease or hold it steady. It was first demonstrated for FIFO replacement. LRU is provably immune to this anomaly because it satisfies the stack property: the set of blocks held by an LRU cache of size kk is always a superset of the blocks held by an LRU cache of size k′<kk' < k, for the same reference stream. That inclusion guarantee makes cache-size monotonicity automatic for LRU, but FIFO (and some other non-stack policies) offer no such guarantee.

How does pseudo-LRU actually decide a victim without a full recency order?

Pseudo-LRU (PLRU) arranges the nn ways of a set as leaves of a binary tree with n−1n-1 internal decision bits. Each internal bit records which half of its subtree was not the most recently accessed. On an access to a given way, the controller walks from the root to that leaf, flipping each bit along the path to point away from the accessed half. On eviction, the controller walks the same tree, following each bit's "point-away" direction, and lands on a leaf that was not recently used — though not necessarily the single least-recently-used way, since the tree only encodes a coarse ordering. This gives O(log⁡n)O(\log n) update and lookup cost instead of LRU's O(n)O(n) or O(n2)O(n^2), at the cost of occasionally evicting a block that is not truly the least recently used.

How does replacement policy relate to the "three Cs" of cache misses?

The three Cs classify misses as compulsory (first-ever access to a block), capacity (the working set exceeds total cache size), or conflict (the working set would fit, but maps to too few ways in one set). Replacement policy quality directly determines how many conflict misses occur: a smarter policy (closer to Belady's MIN) evicts blocks that truly won't be reused soon, minimizing conflict misses for a given associativity, while a poor policy evicts soon-to-be-reused blocks and inflates the conflict-miss count even though total working-set size hasn't changed. Compulsory and capacity misses are essentially unaffected by replacement policy — they depend on block size and total capacity, not on which victim gets chosen.

Does replacement policy interact with whether a cache is write-back or write-through?

Indirectly, yes. Under a write-back policy, a "dirty" evicted block must first be written to the next level of memory before its way can be reused, adding latency exactly at eviction time — so a policy's eviction frequency (driven by miss rate) and timing both matter more under write-back than under write-through, where evicted blocks are already consistent with lower levels. This connects replacement policy choices directly to the mechanisms covered under Cache Write Policies: a policy causing frequent evictions of recently-written (dirty) blocks can amplify write-back traffic even if its overall miss rate looks acceptable.

What per-set state would I need to add true LRU support for a 4-way cache?

Using the counter-based scheme, each of the 4 ways needs a 2-bit counter (since ⌈log⁡24⌉=2\lceil \log_2 4 \rceil = 2), for 8 bits total per set. On every hit or fill to way ww, you compare ww's old counter against every other way's counter, incrementing those with a smaller value, then reset ww's counter to 0. On a miss, the victim is the way whose counter equals 3. Using the matrix scheme instead, you'd need (42)=6\binom{4}{2}=6 bits per set, updating one row and one column per access. Either is entirely tractable at 4-way associativity, which is why exact LRU remains common at this size in real L1 and L2 caches.

Before this

Where this leads