Skip to main content

kovan_map/
hashmap.rs

1//! High-Performance Lock-Free Concurrent Hash Map (FoldHash + resizable table).
2//!
3//! # Strategy
4//!
5//! 1. **FoldHash**: `foldhash::fast::FixedState` for fast, quality hashing.
6//! 2. **Resizable bucket table**: the bucket array lives in a single
7//!    allocation (header + inline buckets) swapped atomically on resize and
8//!    reclaimed through kovan. The map grows when the load factor exceeds
9//!    3/4 and shrinks below 1/4 (never under its initial capacity), the
10//!    same thresholds as `HopscotchMap`.
11//! 3. **Node layout**: `hash` and `next` side by side, so a walk passing a
12//!    node reads one cache line of it.
13//!
14//! # Architecture
15//! - **Table**: kovan-retired object holding the bucket array (atomic head
16//!   pointers) and its entry count. Readers snapshot the table under a guard
17//!   and never block.
18//! - **Chains**: singly linked, lock-free. Every change of the map's content
19//!   is one CAS on one link word (`node`): an insert links a new node at the
20//!   tail, a remove marks the node's own `next`, a replace marks it naming
21//!   the new node, which names the old successor. That CAS is the
22//!   operation's linearization point, and the answer the operation gives is
23//!   exact: `insert_if_absent` answers `None` exactly when its CAS linked
24//!   the key's node, and otherwise the value of the node it found live.
25//! - **Conditional writes** (`conditional`): their closure runs once, so
26//!   they hold the key's link word (a flag set by one CAS) while it runs and
27//!   then write the link with one store; every other writer's CAS fails on a
28//!   held link and walks again, readers never wait.
29//! - **Reclamation**: a node is retired only by the thread whose CAS unlinked
30//!   it, never while a table can reach it, and a walk steps past a deleted
31//!   node only after checking the link it came through still names it
32//!   (`walk`), so every node a walk reads is protected by its guard.
33//! - **Resize**: a single resizer (latch `resizing`) freezes every link of
34//!   the old table in chain order while it copies the live nodes, then
35//!   publishes the new table and retires the old one (`resize`). A write
36//!   whose CAS landed is final; a write that meets a frozen link waits for
37//!   the new table and writes there.
38//!
39//! The protocol is modelled in `proofs/tla/chained/ChainedMap.tla` and checked by TLC
40//! with every rule 0.1.20 had put back one at a time (`proofs/tla/chained/README.md`).
41
42extern crate alloc;
43
44#[cfg(feature = "std")]
45extern crate std;
46
47use crate::MaybeSend;
48use crate::sync::AtomicBool;
49use core::borrow::Borrow;
50use core::hash::{BuildHasher, Hash};
51use core::sync::atomic::Ordering;
52use foldhash::fast::FixedState;
53use kovan::{Atomic, Guard, NodeBox, pin, retire_node};
54use node::{MARK, Node, with, word};
55use table::{TableHeader, TableRef};
56use walk::Found;
57
58pub use iter::{IntoIter, Iter, Keys, Values};
59
60mod conditional;
61mod iter;
62mod node;
63mod resize;
64mod std_traits;
65mod table;
66mod walk;
67
68/// Default number of buckets for `new()`. Matches the previous fixed-table
69/// sizing (zero collisions for ~100k items, fits in L3); maps created with
70/// `new()` keep exactly the old memory/performance profile and additionally
71/// grow past it on demand. Use [`HashMap::with_capacity`] for small elastic
72/// maps.
73const DEFAULT_CAPACITY: usize = 524_288;
74
75/// Minimum number of buckets; the map never shrinks below this.
76const MIN_CAPACITY: usize = 64;
77
78// Load-factor thresholds (implemented as integer comparisons on the hot
79// paths): grow when count/capacity > 3/4, shrink when count/capacity < 1/4
80// (never below the map's floor capacity). Same thresholds as HopscotchMap.
81
82/// A simple exponential backoff for reducing contention.
83struct Backoff {
84    step: u32,
85}
86
87impl Backoff {
88    #[inline(always)]
89    fn new() -> Self {
90        Self { step: 0 }
91    }
92
93    #[inline(always)]
94    fn spin(&mut self) {
95        // Under shuttle, one yield: a writer whose CAS failed on a held link waits for the
96        // holder, which must get to run (a spin with no yield in it can starve it under PCT).
97        #[cfg(feature = "shuttle")]
98        crate::sync::spin_hint();
99        #[cfg(not(feature = "shuttle"))]
100        for _ in 0..(1 << self.step.min(6)) {
101            core::hint::spin_loop();
102        }
103        if self.step <= 6 {
104            self.step += 1;
105        }
106    }
107}
108
109/// High-Performance Lock-Free Map with automatic grow/shrink.
110pub struct HashMap<K: 'static, V: 'static, S = FixedState> {
111    table: Atomic<TableHeader>,
112    /// Single-resizer latch, held by a resize or a clear from before it
113    /// freezes the first link until after it publishes the new table.
114    resizing: AtomicBool,
115    /// Shrink floor: the initial capacity. The map never shrinks below the
116    /// size it was created with, preserving the caller's sizing intent (and
117    /// the historical fixed-table behavior for `new()`).
118    floor: usize,
119    hasher: S,
120    _marker: core::marker::PhantomData<(K, V)>,
121}
122
123/// The node an insert links: its key and value until an attempt needs the allocation, then that
124/// allocation, carried across the attempts that fail to link it, so a retry neither clones the
125/// key and value nor allocates again, and a losing call drops them exactly once.
126enum Pending<K, V> {
127    Parts { key: K, value: V },
128    Built(NodeBox<Node<K, V>>),
129}
130
131impl<K, V> Pending<K, V> {
132    #[inline(always)]
133    fn key(&self) -> &K {
134        match self {
135            Self::Parts { key, .. } => key,
136            Self::Built(node) => &node.key,
137        }
138    }
139
140    /// The node, allocated the first time an attempt needs it, its `next` set to `next`.
141    #[inline]
142    fn into_node(self, hash: u64, next: *mut Node<K, V>) -> *mut Node<K, V> {
143        let node = match self {
144            Self::Parts { key, value } => NodeBox::new(Node::new(hash, key, value)),
145            Self::Built(node) => node,
146        };
147        // Relaxed: the node is private until the CAS that links it releases it.
148        node.next.store(word(next), Ordering::Relaxed);
149        NodeBox::into_raw(node)
150    }
151
152    /// The node an attempt failed to link, back for the next attempt.
153    #[inline]
154    fn back(node: *mut Node<K, V>) -> Self {
155        // SAFETY: the CAS that would have published `node` failed, so it is still the
156        // allocation `into_node` gave this call.
157        Self::Built(unsafe { NodeBox::from_raw(node) })
158    }
159}
160
161/// How a conditional insert ended.
162enum Claim<R, V> {
163    /// This call linked the key's node; what the caller asked of the value it linked.
164    Inserted(R),
165    /// The key was present; its value.
166    Present(V),
167}
168
169// Small accessors that never hash: only the struct's own `'static` bound, as std's equivalent
170// block for `with_hasher`/`with_capacity_and_hasher`/`capacity`/`len`/`is_empty`/`hasher` needs
171// no `Hash`, `Eq`, `Clone` or `BuildHasher`. A method that hashes or clones a value lives in the
172// bound impl block below instead.
173impl<K: 'static, V: 'static, S> HashMap<K, V, S> {
174    /// Creates a new hash map with custom hasher.
175    pub fn with_hasher(hasher: S) -> Self {
176        Self::with_capacity_and_hasher(DEFAULT_CAPACITY, hasher)
177    }
178
179    /// Creates a new hash map with at least `capacity` buckets and a custom hasher.
180    ///
181    /// The map grows when its load factor exceeds 0.75 and shrinks when it
182    /// falls below 0.25 - but never below `capacity`.
183    pub fn with_capacity_and_hasher(capacity: usize, hasher: S) -> Self {
184        let table = TableRef::<K, V>::alloc(capacity);
185        let floor = table.capacity();
186        Self {
187            table: Atomic::new(table.as_raw()),
188            resizing: AtomicBool::new(false),
189            floor,
190            hasher,
191            _marker: core::marker::PhantomData,
192        }
193    }
194
195    /// Returns the current number of buckets.
196    pub fn capacity(&self) -> usize {
197        let guard = pin();
198        self.table_ref(&guard).capacity()
199    }
200
201    /// Returns true if the map is empty.
202    pub fn is_empty(&self) -> bool {
203        self.len() == 0
204    }
205
206    /// Returns the number of elements in the map.
207    ///
208    /// O(1): the current table's count, kept by insert/remove and set exactly
209    /// by a resize. Approximate while concurrent updates are in flight (a
210    /// write counts itself right after its CAS), exact in quiescence.
211    pub fn len(&self) -> usize {
212        let guard = pin();
213        self.table_ref(&guard)
214            .count()
215            .load(Ordering::Relaxed)
216            .max(0) as usize
217    }
218
219    /// Get the underlying hasher itself.
220    pub fn hasher(&self) -> &S {
221        &self.hasher
222    }
223}
224
225// Construction with the built-in hasher never hashes either: the struct's own `'static` only,
226// as std's `HashMap::new`/`with_capacity` carry no bound.
227#[cfg(feature = "std")]
228impl<K: 'static, V: 'static> HashMap<K, V, FixedState> {
229    /// Creates a new empty hash map with FoldHash (FixedState).
230    pub fn new() -> Self {
231        Self::with_hasher(FixedState::default())
232    }
233
234    /// Creates a new empty hash map with at least `capacity` buckets.
235    pub fn with_capacity(capacity: usize) -> Self {
236        Self::with_capacity_and_hasher(capacity, FixedState::default())
237    }
238}
239
240impl<K, V, S> HashMap<K, V, S>
241where
242    K: Hash + Eq + Clone + MaybeSend + 'static,
243    V: Clone + MaybeSend + 'static,
244    S: BuildHasher,
245{
246    /// Returns the value of `key`. Never blocks: reads the current table
247    /// under a guard, even while a resize is in flight.
248    ///
249    /// Linearizable: the answer is the value of a node that was not deleted
250    /// at the moment its `next` was read, or `None` when the key's chain
251    /// held no such node.
252    pub fn get<Q>(&self, key: &Q) -> Option<V>
253    where
254        K: Borrow<Q>,
255        Q: Hash + Eq + ?Sized,
256    {
257        let hash = self.hasher.hash_one(key);
258        let guard = pin();
259        self.lookup(hash, key, &guard)
260            .map(|node| node.value.clone())
261    }
262
263    /// Checks if the key exists (without cloning its value).
264    pub fn contains_key<Q>(&self, key: &Q) -> bool
265    where
266        K: Borrow<Q>,
267        Q: Hash + Eq + ?Sized,
268    {
269        let hash = self.hasher.hash_one(key);
270        let guard = pin();
271        self.lookup(hash, key, &guard).is_some()
272    }
273
274    /// Inserts a key-value pair, returning the value it replaced.
275    ///
276    /// Linearizable at the one CAS that links the new node (a new key) or
277    /// marks the old node naming the new one (a present key); the answer is
278    /// exactly the value that CAS replaced.
279    pub fn insert(&self, key: K, value: V) -> Option<V> {
280        let hash = self.hasher.hash_one(&key);
281        let mut pending = Pending::Parts { key, value };
282        let mut backoff = Backoff::new();
283        loop {
284            let guard = pin();
285            let (table, found) = self.find(hash, pending.key(), &guard);
286            match found {
287                Found::Frozen => {
288                    drop(guard);
289                    self.wait_for_resize();
290                }
291                Found::Miss { tail } => {
292                    let node = pending.into_node(hash, core::ptr::null_mut());
293                    // Release: a reader that acquires the link sees the node's fields.
294                    match tail.compare_exchange(
295                        word(core::ptr::null_mut()),
296                        word(node),
297                        Ordering::Release,
298                        Ordering::Relaxed,
299                        &guard,
300                    ) {
301                        Ok(_) => {
302                            self.landed(table, guard);
303                            return None;
304                        }
305                        Err(_) => {
306                            pending = Pending::back(node);
307                            backoff.spin();
308                        }
309                    }
310                }
311                Found::Hit {
312                    prev,
313                    node: old,
314                    next,
315                } => {
316                    // Cloned before the CAS: a panicking clone leaves the map as it was.
317                    let previous = old.value.clone();
318                    let node = pending.into_node(hash, next);
319                    // The replace: the old node deleted, naming the new node, in one CAS.
320                    // AcqRel: releases the new node; acquires what the old word carried.
321                    match old.next.compare_exchange(
322                        word(next),
323                        word(with(node, MARK)),
324                        Ordering::AcqRel,
325                        Ordering::Relaxed,
326                        &guard,
327                    ) {
328                        Ok(_) => {
329                            self.unlink(prev, old, node, hash, &old.key, &guard);
330                            return Some(previous);
331                        }
332                        Err(_) => {
333                            pending = Pending::back(node);
334                            backoff.spin();
335                        }
336                    }
337                }
338            }
339        }
340    }
341
342    /// Insert a key-value pair only if the key does not exist.
343    /// Returns `None` if inserted, `Some(existing_value)` if the key already exists.
344    ///
345    /// Exact: `None` exactly when this call linked the key's node (its CAS is
346    /// the linearization point), `Some` with the value of the node it found
347    /// live otherwise; never both, and a resize in flight changes neither.
348    /// A call that finds the key present drops its own key and value, once.
349    pub fn insert_if_absent(&self, key: K, value: V) -> Option<V> {
350        match self.claim(key, value, |_| ()) {
351            Claim::Inserted(()) => None,
352            Claim::Present(existing) => Some(existing),
353        }
354    }
355
356    /// Returns the value corresponding to the key, or inserts the given value if the key is not present.
357    ///
358    /// Linearizable and exact: of the callers racing for an absent key, the
359    /// one whose CAS links the key's node gets its own value back, and every
360    /// other gets the value it found live (the winner's, unless a later
361    /// write already replaced it). One clone of the value either way.
362    pub fn get_or_insert(&self, key: K, value: V) -> V {
363        match self.claim(key, value, V::clone) {
364            Claim::Inserted(own) | Claim::Present(own) => own,
365        }
366    }
367
368    /// The conditional insert both `insert_if_absent` and `get_or_insert`
369    /// are: link the key's node unless the key is present. `on_insert` reads
370    /// the value this call linked, under its guard.
371    fn claim<R>(&self, key: K, value: V, on_insert: impl FnOnce(&V) -> R) -> Claim<R, V> {
372        let hash = self.hasher.hash_one(&key);
373        let mut pending = Pending::Parts { key, value };
374        let mut backoff = Backoff::new();
375        loop {
376            let guard = pin();
377            let (table, found) = self.find(hash, pending.key(), &guard);
378            match found {
379                Found::Frozen => {
380                    drop(guard);
381                    self.wait_for_resize();
382                }
383                // Present: the node was not deleted when the walk read its `next`.
384                Found::Hit { node, .. } => return Claim::Present(node.value.clone()),
385                Found::Miss { tail } => {
386                    let node = pending.into_node(hash, core::ptr::null_mut());
387                    // Release: a reader that acquires the link sees the node's fields.
388                    match tail.compare_exchange(
389                        word(core::ptr::null_mut()),
390                        word(node),
391                        Ordering::Release,
392                        Ordering::Relaxed,
393                        &guard,
394                    ) {
395                        Ok(_) => {
396                            // SAFETY: this call allocated and linked it under `guard`, which
397                            // keeps it from being freed.
398                            let answer = on_insert(unsafe { &(*node).value });
399                            self.landed(table, guard);
400                            return Claim::Inserted(answer);
401                        }
402                        Err(_) => {
403                            pending = Pending::back(node);
404                            backoff.spin();
405                        }
406                    }
407                }
408            }
409        }
410    }
411
412    /// Remove a key-value pair, returning its value.
413    ///
414    /// Linearizable at the CAS that marks the key's node deleted; the answer
415    /// is that node's value, and a later `get` of the key (with no insert of
416    /// it in between) answers `None`. [`remove_if`](Self::remove_if) with a
417    /// predicate that always holds, which needs no hold of the node: one
418    /// write path for both.
419    pub fn remove<Q>(&self, key: &Q) -> Option<V>
420    where
421        K: Borrow<Q>,
422        Q: Hash + Eq + ?Sized,
423    {
424        self.remove_where::<Q, false>(key, |_| true)
425    }
426
427    /// Remove the key's entry, returning its value if the key was present.
428    ///
429    /// A key has at most one live node, so this removes what [`remove`](Self::remove)
430    /// removes; it keeps removing until a call answers `None` (the key is
431    /// absent at that call's linearization point), and it is kept for
432    /// parity with `HopscotchMap::force_remove`.
433    ///
434    /// Note: a concurrent `insert` of the same key can land after the final
435    /// scan, as with any removal under contention.
436    pub fn force_remove<Q>(&self, key: &Q) -> Option<V>
437    where
438        K: Borrow<Q>,
439        Q: Hash + Eq + ?Sized,
440    {
441        let mut newest = None;
442        loop {
443            match self.remove(key) {
444                Some(v) => {
445                    if newest.is_none() {
446                        newest = Some(v);
447                    }
448                }
449                None => return newest,
450            }
451        }
452    }
453
454    /// Insert all `(K, V)` pairs from `iter`. Takes `&self` (concurrent map).
455    pub fn extend<I: IntoIterator<Item = (K, V)>>(&self, iter: I) {
456        for (k, v) in iter {
457            self.insert(k, v);
458        }
459    }
460
461    /// A new key landed in `table`: count it there, and grow the table past three quarters.
462    #[inline]
463    fn landed(&self, table: TableRef<K, V>, guard: Guard) {
464        let count = table.count().fetch_add(1, Ordering::Relaxed) + 1;
465        let capacity = table.capacity();
466        // Integer load-factor check: count/cap > 3/4.
467        if 4 * (count.max(0) as usize) > 3 * capacity {
468            drop(guard);
469            self.try_resize(capacity * 2);
470        }
471    }
472
473    /// Unlink the deleted `node` from `prev`, which named it unmarked, in favour of `succ`. The
474    /// thread whose CAS unlinks a node retires it; when this CAS fails (another snip, or the
475    /// predecessor deleted too), the cleanup walk makes sure the node is unlinked before the
476    /// caller returns.
477    #[inline]
478    fn unlink<Q>(
479        &self,
480        prev: &Atomic<Node<K, V>>,
481        node: &Node<K, V>,
482        succ: *mut Node<K, V>,
483        hash: u64,
484        key: &Q,
485        guard: &Guard,
486    ) where
487        K: Borrow<Q>,
488        Q: Eq + ?Sized,
489    {
490        let raw = node as *const Node<K, V> as *mut Node<K, V>;
491        // AcqRel: as a snip (see `walk::find`).
492        if prev
493            .compare_exchange(
494                word(raw),
495                word(succ),
496                Ordering::AcqRel,
497                Ordering::Relaxed,
498                guard,
499            )
500            .is_ok()
501        {
502            // SAFETY: this CAS unlinked it, so no other thread retires it and no link names it
503            // again; a walker that loaded it holds a guard that keeps it. Node is #[repr(C)]
504            // with its RetiredNode at offset 0.
505            unsafe { retire_node(raw) };
506        } else {
507            self.cleanup(hash, key, guard);
508        }
509    }
510}
511
512// SAFETY: HashMap is Send if K, V, S are Send (moving ownership between threads).
513// HashMap is Sync if K, V, S are Send+Sync. The stronger bound on Sync is needed
514// because concurrent `get()` calls clone V through a `&V` reference across threads;
515// if V were Send but not Sync, sharing `&HashMap` could transmit a non-Sync V reference
516// to another thread via clone(), violating thread-safety.
517unsafe impl<K: Send, V: Send, S: Send> Send for HashMap<K, V, S> {}
518unsafe impl<K: Send + Sync, V: Send + Sync, S: Send + Sync> Sync for HashMap<K, V, S> {}
519
520impl<K: 'static, V: 'static, S> Drop for HashMap<K, V, S> {
521    fn drop(&mut self) {
522        // SAFETY: `drop(&mut self)` guarantees exclusive ownership - no concurrent
523        // readers can exist. Rust's type system enforces this: `Iter<'a, ...>` borrows
524        // `&'a HashMap`, so it cannot outlive the `HashMap`. The Table's destructor
525        // frees its chains.
526        let guard = pin();
527        let table = TableRef::<K, V>::from_raw(self.table.load(Ordering::Relaxed, &guard).as_raw());
528        drop(guard);
529        // SAFETY: exclusive access; frees remaining chains + the allocation.
530        unsafe { table.free() };
531
532        // Submit what this thread retired (its batch), if anything.
533        kovan::flush_pending();
534    }
535}
536
537#[cfg(test)]
538mod tests;