Skip to content

Commit 2375826

Browse files
authored
Refactor LRU policy to use a pool-based design for allocation-free performance (#36)
- Transitioned to a `SlotArena` for contiguous storage of nodes, enabling O(1) slot reuse and eliminating heap allocations during steady state. - Updated core data structures to utilize `FxHashMap` for key-to-slot lookup and maintain a doubly-linked order via `SlotId` indices. - Enhanced concurrency support with a `ConcurrentLruCache` wrapper, detailing read/write lock requirements for operations. - Revised documentation to reflect architectural changes, including node layout, allocation strategy, and safety invariants, ensuring clarity on performance and usage.
1 parent 79bf09b commit 2375826

2 files changed

Lines changed: 230 additions & 284 deletions

File tree

docs/policies/lru.md

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,48 +6,54 @@
66
Evict the entry that has not been accessed for the longest time.
77

88
## 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)
129

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.
1821

1922
## Operations
2023

2124
### `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>`.
2326

2427
### `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.
2629
- 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.
2933

3034
### `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.
3239

3340
## 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)
3644

3745
## 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.
4551

4652
## 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`.
5157

5258
## References
5359
- Wikipedia: https://en.wikipedia.org/wiki/Cache_replacement_policies

0 commit comments

Comments
 (0)