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;