Skip to main content

kovan_map/
hopscotch.rs

1//! Lock-Free Growing/Shrinking Hopscotch Hash Map
2//!
3//! # Features
4//!
5//! - **Lock-free reads**: a lookup and a walk take no guard and never wait for a writer.
6//! - **Home-bucket writer guards**: every write that links, replaces, unlinks or moves an entry
7//!   holds the writer guard of the entry's home bucket, so a writer's scan of its home is stable
8//!   and a key never has two entries.
9//! - **Displacement without a gap**: an insert whose neighborhood is full moves entries of other
10//!   homes (the same allocation, not a copy) under their home guards. A moved entry is linked at
11//!   its new slot before it is unlinked from the old one, and its home's move stamp advances in
12//!   between, so a lookup that raced the move rescans instead of missing the key, and a walk
13//!   meets the entry and yields it once.
14//! - **Safe Resizing**: A resize (and a clear) takes every home bucket's writer guard of the
15//!   table before it copies a slot, so a write either lands before the copy (and is copied) or
16//!   waits for the new table; a table a resize replaced never admits a writer again. An insert
17//!   that landed is final: it never retries into the new table, where it would meet its own
18//!   migrated entry and report it as another caller's.
19//! - **Stable iteration**: An iterator walks the table that was current when it was created to
20//!   the end, even when a resize replaces it meanwhile.
21//! - **Memory reclamation**: Uses Kovan.
22//! - **Clone Support**: Supports V: Clone (e.g., `Arc<T>`) instead of just Copy.
23
24extern crate alloc;
25
26use crate::sync::spin_hint;
27use crate::sync::{AtomicBool, AtomicUsize};
28use alloc::boxed::Box;
29use core::borrow::Borrow;
30use core::hash::{BuildHasher, Hash};
31use core::sync::atomic::Ordering;
32use displace::{InsertResult, Pending};
33use foldhash::fast::FixedState;
34use kovan::{Atomic, CachePadded, pin, retire};
35use table::{HOP_MASK, Table, Word, hop_bits};
36
37pub use iter::{HopscotchIntoIter, HopscotchIter, HopscotchKeys, HopscotchValues};
38
39mod displace;
40mod iter;
41mod resize;
42mod std_traits;
43mod table;
44
45/// Neighborhood size (H parameter in hopscotch hashing)
46const NEIGHBORHOOD_SIZE: usize = 32;
47
48/// Initial capacity
49const INITIAL_CAPACITY: usize = 64;
50
51/// Whether `count` entries overfill a table of `capacity`: more than three quarters full.
52#[inline(always)]
53fn overfull(count: usize, capacity: usize) -> bool {
54    4 * count > 3 * capacity
55}
56
57/// Whether `count` entries underfill a table of `capacity`: less than a quarter full, and the
58/// table larger than the least one.
59#[inline(always)]
60fn underfull(count: usize, capacity: usize) -> bool {
61    4 * count < capacity && capacity > MIN_CAPACITY
62}
63
64/// Minimum capacity to prevent excessive shrinking
65const MIN_CAPACITY: usize = 64;
66
67/// Maximum probe distance before giving up and resizing
68const MAX_PROBE_DISTANCE: usize = 512;
69
70/// A concurrent, lock-free hash map based on Hopscotch Hashing.
71pub struct HopscotchMap<K: 'static, V: 'static, S = FixedState> {
72    table: Atomic<Table<K, V>>,
73    /// The entries in the map, on a cache line of its own: every write that links or unlinks an
74    /// entry bumps it, and every operation reads the table pointer and the resize flag, which
75    /// would otherwise share its line.
76    count: CachePadded<AtomicUsize>,
77    /// Prevents concurrent writes during resize migration to avoid lost updates
78    resizing: AtomicBool,
79    hasher: S,
80}
81
82/// How a write of `insert_impl` ended.
83enum Outcome<R, V> {
84    /// The key was absent: this call linked its entry; what the caller asked of its value.
85    Linked(R),
86    /// The key was present and this call replaced its entry: the old value.
87    Replaced(V),
88    /// The key was present and this call only claims an absent key: its value.
89    Present(V),
90}
91
92// Small accessors that never hash: only the struct's own `'static` bound, as std's equivalent
93// block for `with_hasher`/`with_capacity_and_hasher`/`capacity`/`len`/`is_empty`/`hasher` needs
94// no `Hash`, `Eq`, `Clone` or `BuildHasher`. A method that hashes or clones a value lives in the
95// bound impl block below instead.
96impl<K: 'static, V: 'static, S> HopscotchMap<K, V, S> {
97    /// Creates a new `HopscotchMap` with the specified hasher and default capacity.
98    pub fn with_hasher(hasher: S) -> Self {
99        Self::with_capacity_and_hasher(INITIAL_CAPACITY, hasher)
100    }
101
102    /// Creates a new `HopscotchMap` with the specified capacity and hasher.
103    pub fn with_capacity_and_hasher(capacity: usize, hasher: S) -> Self {
104        let table = Table::new(capacity);
105        Self {
106            table: Atomic::new(Box::into_raw(Box::new(table))),
107            count: CachePadded::new(AtomicUsize::new(0)),
108            resizing: AtomicBool::new(false),
109            hasher,
110        }
111    }
112
113    /// Returns the number of elements in the map.
114    pub fn len(&self) -> usize {
115        self.count.load(Ordering::Relaxed)
116    }
117
118    /// Returns `true` if the map contains no elements.
119    pub fn is_empty(&self) -> bool {
120        self.len() == 0
121    }
122
123    /// Returns the current capacity of the map.
124    pub fn capacity(&self) -> usize {
125        let guard = pin();
126        let table_ptr = self.table.load(Ordering::Acquire, &guard);
127        unsafe { (*table_ptr.as_raw()).capacity }
128    }
129
130    /// Get the underlying hasher.
131    pub fn hasher(&self) -> &S {
132        &self.hasher
133    }
134}
135
136// Construction with the built-in hasher never hashes either: the struct's own `'static` only,
137// as std's `HashMap::new`/`with_capacity` carry no bound.
138#[cfg(feature = "std")]
139impl<K: 'static, V: 'static> HopscotchMap<K, V, FixedState> {
140    /// Creates a new `HopscotchMap` with default capacity and hasher.
141    pub fn new() -> Self {
142        Self::with_hasher(FixedState::default())
143    }
144
145    /// Creates a new `HopscotchMap` with the specified capacity and default hasher.
146    pub fn with_capacity(capacity: usize) -> Self {
147        Self::with_capacity_and_hasher(capacity, FixedState::default())
148    }
149}
150
151impl<K, V, S> HopscotchMap<K, V, S>
152where
153    K: Hash + Eq + Clone + 'static,
154    V: Clone + 'static,
155    S: BuildHasher,
156{
157    #[inline]
158    fn wait_for_resize(&self) {
159        while self.resizing.load(Ordering::Acquire) {
160            spin_hint();
161        }
162    }
163
164    /// Returns the value corresponding to the key.
165    pub fn get<Q>(&self, key: &Q) -> Option<V>
166    where
167        K: Borrow<Q>,
168        Q: Hash + Eq + ?Sized,
169    {
170        self.get_hashed(self.hasher.hash_one(key), key)
171    }
172
173    /// [`get`](Self::get) of a key whose hash the caller already took.
174    #[inline(always)]
175    fn get_hashed<Q>(&self, hash: u64, key: &Q) -> Option<V>
176    where
177        K: Borrow<Q>,
178        Q: Eq + ?Sized,
179    {
180        let guard = pin();
181        let table_ptr = self.table.load(Ordering::Acquire, &guard);
182        let table = unsafe { &*table_ptr.as_raw() };
183
184        table
185            .lookup(hash, key, &guard)
186            .map(|entry| entry.value.clone())
187    }
188
189    /// Inserts a key-value pair into the map, returning the value it replaced.
190    ///
191    /// Linearizable at the write under the key's home guard; the answer is
192    /// exactly the value that write replaced.
193    pub fn insert(&self, key: K, value: V) -> Option<V> {
194        let hash = self.hasher.hash_one(&key);
195        match self.insert_impl(hash, key, value, false, |_| ()) {
196            Outcome::Linked(()) => None,
197            Outcome::Replaced(old) | Outcome::Present(old) => Some(old),
198        }
199    }
200
201    /// The one write path of `insert`, `insert_if_absent` and `get_or_insert`: link the key's
202    /// entry, or (unless `only_if_absent`) replace the present one, under the key's home guard.
203    /// `on_insert` reads the value this call linked.
204    fn insert_impl<R>(
205        &self,
206        hash: u64,
207        key: K,
208        value: V,
209        only_if_absent: bool,
210        on_insert: impl FnOnce(&V) -> R,
211    ) -> Outcome<R, V> {
212        let mut pending = Pending::Parts { hash, key, value };
213
214        loop {
215            self.wait_for_resize();
216
217            let guard = pin();
218            let table_ptr = self.table.load(Ordering::Acquire, &guard);
219            let table = unsafe { &*table_ptr.as_raw() };
220
221            if self.resizing.load(Ordering::Acquire) {
222                continue;
223            }
224
225            // Home-bucket writer guard: serialize the writes of one home so the existence scan
226            // and the slot claim inside try_insert are one atomic step, and no displacement
227            // moves an entry of the home meanwhile. Contended -> spin via the outer loop, which
228            // keeps re-checking `resizing` and reloads the table. A resize takes every home
229            // guard of the table before it copies a slot (`hold_writers`) and keeps the guards
230            // of a table it replaced, so holding this guard means the table is live and every
231            // slot this call writes is copied by any resize that follows. No lock-order
232            // deadlock: a guard holder never waits for anything (it takes the guards of other
233            // homes it displaces entries of without waiting).
234            let Some(mut home) = table.home_guard(table.bucket_index(hash)) else {
235                #[cfg(test)]
236                pause::at(pause::Point::WriterMetHeldGuard);
237                // A claim answers a key the holder linked meanwhile without waiting for the
238                // guard, from the same lookup.
239                if only_if_absent && let Some(entry) = table.lookup(hash, pending.key(), &guard) {
240                    return Outcome::Present(entry.value.clone());
241                }
242                spin_hint();
243                continue;
244            };
245
246            let outcome = Self::try_insert(table, &mut home, pending, only_if_absent, &guard);
247            // A new entry is counted once, before its home guard is released: concurrent
248            // removes cannot decrement the count below the true entry count, and a clear
249            // (which resets the count while it holds every home guard) never sees the entry
250            // without its count.
251            let new_count = match outcome {
252                InsertResult::Linked(_) => Some(self.count.fetch_add(1, Ordering::Relaxed) + 1),
253                _ => None,
254            };
255            // The guard is released (publishing the new entry's hop bit) before the resize arms
256            // below drop the pin and migrate: the release touches this table's bucket, which is
257            // only safe while the pin keeps the table alive.
258            drop(home);
259            match outcome {
260                InsertResult::Linked(entry) => {
261                    #[cfg(test)]
262                    pause::at(pause::Point::AfterLanding);
263                    // Final. The write landed in a live table under its home
264                    // guard, so a resize that starts after it copies it; a
265                    // retry here would meet this call's own migrated entry
266                    // and report it as present (`insert_if_absent` answering
267                    // `Some(own value)` for an insert that happened).
268                    // SAFETY: linked by this call under `guard`, which keeps it (and its table)
269                    // from being freed even if a writer unlinks it now.
270                    let answer = on_insert(unsafe { &(*entry).value });
271                    if let Some(new_count) = new_count
272                        && overfull(new_count, table.capacity)
273                    {
274                        let current_capacity = table.capacity;
275                        drop(guard);
276                        self.try_resize(current_capacity * 2);
277                    }
278                    return Outcome::Linked(answer);
279                }
280                InsertResult::Replaced(old) => return Outcome::Replaced(old),
281                InsertResult::Exists(existing) => return Outcome::Present(existing),
282                InsertResult::NeedResize(back) => {
283                    pending = back;
284                    let current_capacity = table.capacity;
285                    drop(guard);
286                    self.try_resize(current_capacity * 2);
287                }
288                InsertResult::Retry(back) => {
289                    pending = back;
290                    spin_hint();
291                }
292            }
293        }
294    }
295
296    /// Returns the value corresponding to the key, or inserts the given value if the key is not present.
297    ///
298    /// Linearizable and exact: of the callers racing for an absent key, the
299    /// one whose write links the key's entry (under the key's home guard)
300    /// gets its own value back, and every other gets the value it found
301    /// there (the winner's, unless a later write already replaced it). A
302    /// present key is answered by a lookup that takes no guard.
303    pub fn get_or_insert(&self, key: K, value: V) -> V {
304        // A present key is answered by the lookup `get` makes, before any write step: this call
305        // writes nothing then, so it takes effect at that lookup. The hash is taken once.
306        let hash = self.hasher.hash_one(&key);
307        if let Some(v) = self.get_hashed(hash, &key) {
308            return v;
309        }
310        match self.insert_impl(hash, key, value, true, V::clone) {
311            Outcome::Linked(own) | Outcome::Present(own) | Outcome::Replaced(own) => own,
312        }
313    }
314
315    /// Insert a key-value pair only if the key does not exist.
316    /// Returns `None` if inserted, `Some(existing_value)` if the key already exists.
317    ///
318    /// Exact: `None` exactly when this call linked the key's entry, `Some`
319    /// with the value it found under the key's home guard otherwise; never
320    /// both, and a resize in flight changes neither (a write that landed is
321    /// final). A call that finds the key present drops its own key and value,
322    /// once.
323    pub fn insert_if_absent(&self, key: K, value: V) -> Option<V> {
324        // As `get_or_insert`: a present key is answered by a lookup, with the one hash.
325        let hash = self.hasher.hash_one(&key);
326        if let Some(existing) = self.get_hashed(hash, &key) {
327            return Some(existing);
328        }
329        match self.insert_impl(hash, key, value, true, |_| ()) {
330            Outcome::Linked(()) => None,
331            Outcome::Present(existing) | Outcome::Replaced(existing) => Some(existing),
332        }
333    }
334
335    /// Remove **all** nodes matching `key`, returning the most recent value
336    /// if the key was present.
337    ///
338    /// Every write of a home runs under its writer guard, so a key has at
339    /// most one entry and this removes what [`remove`](Self::remove)
340    /// removes. It keeps removing until a scan finds no match, so the key is
341    /// absent at the linearization point of the final scan, and it is kept
342    /// for parity with `HashMap::force_remove`.
343    ///
344    /// Note: a concurrent `insert` of the same key can land after the final
345    /// scan, as with any removal under contention.
346    pub fn force_remove<Q>(&self, key: &Q) -> Option<V>
347    where
348        K: Borrow<Q>,
349        Q: Hash + Eq + ?Sized,
350    {
351        let mut newest = None;
352        loop {
353            match self.remove(key) {
354                Some(v) => {
355                    // The first removal unlinks the first match in scan
356                    // order - the live (most recent) version.
357                    if newest.is_none() {
358                        newest = Some(v);
359                    }
360                }
361                None => return newest,
362            }
363        }
364    }
365
366    /// Removes a key from the map, returning the value at the key if the key was previously in the map.
367    pub fn remove<Q>(&self, key: &Q) -> Option<V>
368    where
369        K: Borrow<Q>,
370        Q: Hash + Eq + ?Sized,
371    {
372        let hash = self.hasher.hash_one(key);
373
374        loop {
375            self.wait_for_resize();
376
377            let guard = pin();
378            let table_ptr = self.table.load(Ordering::Acquire, &guard);
379            let table = unsafe { &*table_ptr.as_raw() };
380
381            if self.resizing.load(Ordering::Acquire) {
382                continue;
383            }
384
385            let home_idx = table.bucket_index(hash);
386            // A home without hop bits has no entry to remove, and seeing that takes no guard:
387            // an entry's bit stays set from its publication to its unlink. Relaxed: no slot is
388            // read.
389            let word = table.get_bucket(home_idx).control.load(Ordering::Relaxed);
390            if hop_bits(word) == 0 {
391                return None;
392            }
393
394            // The home guard, as an insert takes it: the scan below is stable, the unlink cannot
395            // race an update or a move of the entry, and a resize copies the table either before
396            // this call takes the guard (and this call then waits for the new table) or after
397            // the unlink.
398            let Some(mut home) = table.home_guard(home_idx) else {
399                #[cfg(test)]
400                pause::at(pause::Point::WriterMetHeldGuard);
401                spin_hint();
402                continue;
403            };
404            let (offset, word) = table.find(home_idx, home.hops(), hash, key, &guard)?;
405            let entry_ptr = word.ptr();
406            // SAFETY: loaded under `guard`, which keeps it from being freed.
407            let old_value = unsafe { &*entry_ptr }.value.clone();
408            // A store, not a CAS: no other thread writes an occupied slot of a home whose guard
409            // this call holds. Release: a reader that acquires the free slot sees everything this
410            // call wrote before it.
411            table
412                .get_bucket(home_idx + offset)
413                .store(Word::free(), Ordering::Release);
414            home.stage_unlinked(offset);
415
416            // Counted down before the home guard is released, as an insert counts up: a clear
417            // resets the count while it holds every home guard, so no decrement for an entry it
418            // already cleared lands after the reset and eats the count of a later insert.
419            // One subtract, no saturation: the entry this call unlinked was counted when it was
420            // linked (before its home guard was released) and no clear ran since (a clear holds
421            // every home guard, this one included), so the count is at least one here.
422            let prev = self.count.fetch_sub(1, Ordering::Relaxed);
423            debug_assert!(prev > 0, "an unlinked entry was never counted");
424            let shrink_to = underfull(prev - 1, table.capacity).then_some(table.capacity / 2);
425            drop(home);
426
427            // SAFETY: unlinked above under its home guard, so no other thread unlinks or
428            // retires it; a reader that loaded it holds a guard that keeps it alive.
429            unsafe { retire(entry_ptr) };
430
431            if let Some(cap) = shrink_to {
432                drop(guard);
433                self.try_resize(cap);
434            }
435            return Some(old_value);
436        }
437    }
438
439    /// Clears the map, removing all key-value pairs.
440    pub fn clear(&self) {
441        while self
442            .resizing
443            .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
444            .is_err()
445        {
446            spin_hint();
447        }
448
449        let guard = pin();
450        let table_ptr = self.table.load(Ordering::Acquire, &guard);
451        let table = unsafe { &*table_ptr.as_raw() };
452        // No insert is between its scan and its claim while the slots are
453        // cleared: one that landed is cleared with the rest, one that did
454        // not lands after the clear with its hop bit intact. No move has an
455        // entry in two slots either (a move holds its entry's home guard).
456        Self::hold_writers(table);
457
458        for i in 0..(table.capacity + NEIGHBORHOOD_SIZE) {
459            let bucket = table.get_bucket(i);
460            let word = bucket.load(Ordering::Acquire, &guard);
461
462            if !word.is_free()
463                && bucket.replace(
464                    word,
465                    Word::free(),
466                    Ordering::Release,
467                    Ordering::Relaxed,
468                    &guard,
469                )
470            {
471                unsafe { retire(word.ptr()) };
472            }
473
474            if i < table.capacity {
475                // Keeps the guard (held by this clear) and the move stamp.
476                let b = table.get_bucket(i);
477                b.control.fetch_and(!HOP_MASK, Ordering::Release);
478            }
479        }
480
481        self.count.store(0, Ordering::Release);
482        Self::release_writers(table);
483        self.resizing.store(false, Ordering::Release);
484    }
485
486    /// Returns `true` if the key is present.
487    pub fn contains_key<Q>(&self, key: &Q) -> bool
488    where
489        K: Borrow<Q>,
490        Q: Hash + Eq + ?Sized,
491    {
492        self.get(key).is_some()
493    }
494
495    /// Insert all `(K, V)` pairs from `iter`. Takes `&self` (concurrent map).
496    pub fn extend<I: IntoIterator<Item = (K, V)>>(&self, iter: I) {
497        for (k, v) in iter {
498            self.insert(k, v);
499        }
500    }
501}
502
503unsafe impl<K: Send, V: Send, S: Send> Send for HopscotchMap<K, V, S> {}
504// SAFETY: Shared references allow moving K and V across threads (via insert/remove),
505// so K and V must be Send in addition to Sync.
506unsafe impl<K: Send + Sync, V: Send + Sync, S: Send + Sync> Sync for HopscotchMap<K, V, S> {}
507
508impl<K, V, S> Drop for HopscotchMap<K, V, S> {
509    fn drop(&mut self) {
510        // SAFETY: `drop(&mut self)` guarantees exclusive ownership - no concurrent
511        // readers can exist. The Table's destructor frees the remaining entries.
512        let guard = pin();
513        let table_ptr = self.table.load(Ordering::Acquire, &guard);
514
515        unsafe {
516            drop(Box::from_raw(table_ptr.as_raw()));
517        }
518
519        // Flush nodes previously retired by concurrent operations (insert/remove/resize)
520        // to prevent use-after-free during process teardown.
521        drop(guard);
522        kovan::flush();
523    }
524}
525
526#[cfg(test)]
527mod pause;
528
529#[cfg(test)]
530mod tests;