Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

LRU Cache — Lookup Plus Recency

An LRU cache evicts the least recently used entry when capacity is full. It combines keyed lookup with an ordering that changes on every hit.

When to use it

Use LRU when recent access predicts future access and storage is bounded. Avoid it when entries have explicit expiration, frequency matters more than recency, or every read becoming a metadata write creates too much contention.

1. Two structures, two questions

HashMap:  where is key K?
Recency:  which key is oldest?

A simple safe implementation uses HashMap<K, V> plus VecDeque<K>. Hits scan the deque, so they are O(n), but the design is often excellent for small caches.

use std::collections::{HashMap, VecDeque};
use std::hash::Hash;

struct Lru<K, V> {
    capacity: usize,
    values: HashMap<K, V>,
    order: VecDeque<K>,
}

impl<K: Clone + Eq + Hash, V> Lru<K, V> {
    fn new(capacity: usize) -> Self {
        assert!(capacity > 0);
        Self { capacity, values: HashMap::new(), order: VecDeque::new() }
    }

    fn get(&mut self, key: &K) -> Option<&V> {
        let position = self.order.iter().position(|stored| stored == key)?;
        let key = self.order.remove(position).unwrap();
        self.order.push_back(key.clone());
        self.values.get(&key)
    }

    fn insert(&mut self, key: K, value: V) {
        if let Some(position) = self.order.iter().position(|stored| stored == &key) {
            self.order.remove(position);
        } else if self.values.len() == self.capacity {
            let oldest = self.order.pop_front().unwrap();
            self.values.remove(&oldest);
        }
        self.order.push_back(key.clone());
        self.values.insert(key, value);
    }
}

fn main() {
    let mut cache = Lru::new(2);
    cache.insert("a", 1);
    cache.insert("b", 2);
    assert_eq!(cache.get(&"a"), Some(&1));
    cache.insert("c", 3);
    assert_eq!(cache.get(&"b"), None);
    assert_eq!(cache.get(&"a"), Some(&1));
}

2. Strict constant time

Strict expected O(1) operations require a hash map pointing to nodes in a doubly linked recency list. In safe Rust, generational arena handles are a natural representation. The map resolves a node; handles unlink and relink it without searching.

3. Alternatives

  • A stale-generation deque log gives amortized O(1) but needs compaction.
  • CLOCK approximates recency with a reference bit.
  • Segmented policies protect frequently reused entries.
  • TTL caches order by expiration rather than access.

4. What you should internalize

LRU is a composition, not one container. “Best” depends on capacity, contention, memory bounds, and whether strict recency is actually required.

Exercise

Trace a capacity-three cache through A, B, C, A, D, C. Record the recency order after each access, then update the example to count hits, misses, and evictions.