Expand description
SharedHashMap<K, V> - cross-process open-addressed hash map
backed by a single MMF file.
§Why open addressing?
All storage is inline. No allocator, no pointer indirection. Each slot lives in its own cache line; the entire table is a flat array in the MMF. Robin Hood, linear, and quadratic probing all work; we use linear probing because it’s the most cache- friendly on modern CPUs (sequential access dominates probe- variance on speculative-prefetch architectures).
§Stable hashing
std::hash::BuildHasher uses a per-process random seed for DoS
resistance, which would make keys irreproducible across
processes. We use FNV-1a over the key bytes - fast, deps-free,
and deterministic across processes / runs / OSes.
§Layout
+---------------------------+
| MapHeader (64B) |
| magic, capacity, count |
| key_size, value_size |
+---------------------------+
| Slot[0] (64B cache line) |
| state (EMPTY/OCC/TS) |
| version (SeqLock) |
| hash (cached) |
| payload [u8; 48]: K + V |
| Slot[1] ... |
+---------------------------+§Protocol
§Insert
- Hash key (FNV-1a).
- Probe from
hash % capacity, linearly. - At each slot:
- Empty: CAS state Empty → Occupied. On success, SeqLock-
write
(K, V)and store hash; bumpcount. Return Inserted. - Occupied & hash matches & key matches: SeqLock-update V (state unchanged). Return Updated.
- Occupied & no match: probe next slot.
- Tombstone: skip (linear probe continues; tombstones do NOT terminate insert because we want to overwrite them preferentially - track first tombstone and use it if no Empty is found earlier than a definitive “not present” conclusion).
- Empty: CAS state Empty → Occupied. On success, SeqLock-
write
Actually the simpler insert: probe until first Empty (insert
there) OR find key (update). The tombstone-reuse optimisation
costs an extra bookkeeping pass; we skip it and reclaim
tombstones via the compact() method (single-writer in-place
rebuild) instead.
§Get
- Hash key, probe linearly.
- Empty: key absent (probe always terminates at Empty).
- Occupied & hash matches: SeqLock-read; if K matches, return V.
- Tombstone or hash mismatch: continue probing.
§Remove
- Find key (same probe).
- CAS state Occupied → Tombstone.
count.fetch_sub(1).
§Concurrency
All slot writes are SeqLock-protected so readers never observe torn key+value. The state byte’s CAS is the serialisation point for who “owns” a slot for write. Two writers racing on the same key both reach the same slot; one wins the Empty→Occupied CAS and writes; the loser falls through to “Occupied + key matches” and updates instead.
§Capacity and load factor
Fixed at create time. Recommend capacity = 2 * expected_max
to keep load factor below 0.5; linear probing degrades sharply
above 0.7. insert returns MapError::Full when the probe
chain saturates.
§If you need dynamic sizing, use SharedUniversal
SharedHashMap deliberately does NOT implement resize-on-grow.
Cross-process resize requires the same reader-coordination
machinery as MMF-backed migration (atomic file rename, reader
re-open signaling). Rather than reinvent that machinery inside
SharedHashMap, callers who need a dynamically-resizing hash
map should use crate::shared_universal::SharedUniversal<T>
configured with hash-map-only backings. The migration mechanism
handles cross-process resize correctly, with the reader-side
generation-bump protocol that makes wrap-around safe.
Structs§
Enums§
Constants§
Functions§
- fnv1a_
64 - FNV-1a 64-bit over a byte slice. Deterministic across processes
(unlike
std::hash::BuildHasherwhich uses per-process random seeds for DoS resistance). - map_
file_ size