|
6 | 6 | Evict the entry that has not been accessed for the longest time. |
7 | 7 |
|
8 | 8 | ## Core Data Structures |
9 | | -Canonical O(1) LRU: |
10 | | -- `HashMap<K, NodeHandle>` mapping keys to nodes |
11 | | -- Intrusive doubly-linked list storing recency order (head = MRU, tail = LRU) |
12 | 9 |
|
13 | | -In `cachekit`, `src/policy/lru.rs` uses: |
14 | | -- `HashMap<K, NonNull<Node<K>>>` for O(1) node access |
15 | | -- an intrusive list (manual `prev`/`next` pointers) to support O(1) move-to-front |
16 | | -- a separate store mapping `K -> Arc<V>` (value ownership) |
17 | | -- an optional concurrent wrapper (`parking_lot::RwLock`) for multi-threaded use |
| 10 | +`src/policy/lru.rs` uses a pool-based design: |
| 11 | + |
| 12 | +- `SlotArena<Node<K, V>>` — contiguous `Vec` with a free list for O(1) slot reuse. |
| 13 | + Each `Node` stores `prev`/`next` as `Option<SlotId>` indices, plus the key and `Arc<V>`. |
| 14 | +- `FxHashMap<K, SlotId>` — O(1) key-to-slot lookup. |
| 15 | +- `head`/`tail: Option<SlotId>` — doubly-linked recency order (head = MRU, tail = LRU). |
| 16 | +- `ConcurrentLruCache` — thread-safe wrapper via `parking_lot::RwLock`. |
| 17 | + |
| 18 | +At steady state (cache full), every insert evicts the tail node (returning its |
| 19 | +slot to the free list) then inserts the new node (reusing a free slot). No heap |
| 20 | +allocations occur after the initial warm-up phase. |
18 | 21 |
|
19 | 22 | ## Operations |
20 | 23 |
|
21 | 24 | ### `get(key)` |
22 | | -- Lookup key; if present: move its node to the head (MRU) and return value. |
| 25 | +- Lookup key in map; if present: detach node, attach at head (MRU), return `&Arc<V>`. |
23 | 26 |
|
24 | 27 | ### `insert(key, value)` |
25 | | -- If `key` exists: update stored value and move node to head. |
| 28 | +- If `key` exists: replace value in-place, detach + attach at head. |
26 | 29 | - Else: |
27 | | - - If at capacity: evict tail node, remove its key from the index and store. |
28 | | - - Insert new node at head and add to index + store. |
| 30 | + - If at capacity: `pop_tail()` removes the LRU node from the arena (slot returned to free list), remove its key from the map. |
| 31 | + - `arena.insert(Node{...})` reuses a free slot or appends. |
| 32 | + - Insert `SlotId` into the map, attach at head. |
29 | 33 |
|
30 | 34 | ### `pop_lru()` |
31 | | -- Remove tail node (LRU) and delete from index + store. |
| 35 | +- Remove tail node from arena + map, return `(K, Arc<V>)`. |
| 36 | + |
| 37 | +### `peek(key)` / `peek_lru()` |
| 38 | +- Read-only lookup; no list reordering. |
32 | 39 |
|
33 | 40 | ## Complexity & Overhead |
34 | | -- Lookup/update/evict: O(1) |
35 | | -- Memory: per-entry list pointers + hash entry + key copy (intrusive list usually stores keys, not values) |
| 41 | +- Lookup / update / evict: O(1) |
| 42 | +- Memory per entry: `SlotId` prev/next (2 × 12 bytes) + key + `Arc<V>` + `Option` discriminant in arena slot |
| 43 | +- Steady-state allocations: **zero** (free-list recycling) |
36 | 44 |
|
37 | 45 | ## Concurrency Notes |
38 | | -Strict global LRU mutates shared metadata on every hit; typical approaches: |
39 | | -- Single lock around the entire structure (simple, but can contend) |
40 | | -- Sharding (key->shard) for scalability |
41 | | -- Approximate LRU (Clock) when write-amplification or contention is too high |
42 | | - |
43 | | -`cachekit` provides a concurrent wrapper in the LRU module; be mindful that: |
44 | | -- every hit does a write (reordering), so read-heavy workloads can still contend. |
| 46 | +Strict global LRU mutates shared metadata on every hit; `cachekit` provides: |
| 47 | +- `ConcurrentLruCache` — single `RwLock` around `LruCore`. |
| 48 | +- `get()` requires a **write** lock (moves node to head). |
| 49 | +- `peek()` requires only a **read** lock (no reordering). |
| 50 | +- Read-heavy workloads can still contend on `get()`; prefer `peek()` when recency updates are not needed. |
45 | 51 |
|
46 | 52 | ## Safety / Invariants (Rust) |
47 | | -If using intrusive pointers (like `NonNull<Node<_>>`): |
48 | | -- Nodes must not move in memory while linked (allocate in a stable arena/Box and manage lifetime). |
49 | | -- Every `prev/next` pointer update must preserve list invariants; always update both sides. |
50 | | -- On removal/eviction, ensure you unlink before freeing and remove from the hashmap index. |
| 53 | +- **No `unsafe`**: all list manipulation uses `SlotId` indices into the arena. |
| 54 | +- **ABA prevention**: `SlotId` includes a generation counter; stale handles return `None`. |
| 55 | +- **Automatic cleanup**: `SlotArena` drops all live nodes when `LruCore` is dropped. |
| 56 | +- `Send`/`Sync` auto-derive from the safe field types; no manual `unsafe impl`. |
51 | 57 |
|
52 | 58 | ## References |
53 | 59 | - Wikipedia: https://en.wikipedia.org/wiki/Cache_replacement_policies |
0 commit comments