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),
10//! mirroring `HopscotchMap`'s resize protocol.
11//! 3. **Optimized Node Layout**: fields ordered `hash -> key -> value -> next`
12//! to optimize cache line usage during checks.
13//!
14//! # Architecture
15//! - **Table**: kovan-retired object holding the bucket array (atomic head
16//! pointers). Readers snapshot the table under a guard and never block.
17//! - **Nodes**: singly linked chains, CAS-based lock-free insert/remove.
18//! - **Resize**: a single resizer (CAS on `resizing`) clones all entries
19//! into a new table, swaps the table pointer, and retires the old table;
20//! the old table's destructor frees its remaining chains exactly once at
21//! reclamation time. Writers wait out an active resize and re-validate
22//! after success so no update is lost to a concurrent migration.
23
24extern crate alloc;
25
26#[cfg(feature = "std")]
27extern crate std;
28
29use alloc::boxed::Box;
30use core::borrow::Borrow;
31use core::hash::{BuildHasher, Hash};
32use core::sync::atomic::{AtomicBool, AtomicIsize, Ordering};
33use foldhash::fast::FixedState;
34use kovan::{Atomic, RetiredNode, Shared, pin, retire};
35
36/// Default number of buckets for `new()`. Matches the previous fixed-table
37/// sizing (zero collisions for ~100k items, fits in L3); maps created with
38/// `new()` keep exactly the old memory/performance profile and additionally
39/// grow past it on demand. Use [`HashMap::with_capacity`] for small elastic
40/// maps.
41const DEFAULT_CAPACITY: usize = 524_288;
42
43/// Minimum number of buckets; the map never shrinks below this.
44const MIN_CAPACITY: usize = 64;
45
46// Load-factor thresholds (implemented as integer comparisons on the hot
47// paths): grow when count/capacity > 3/4, shrink when count/capacity < 1/4
48// (never below the map's floor capacity). Same thresholds as HopscotchMap.
49
50/// A simple exponential backoff for reducing contention.
51struct Backoff {
52 step: u32,
53}
54
55impl Backoff {
56 #[inline(always)]
57 fn new() -> Self {
58 Self { step: 0 }
59 }
60
61 #[inline(always)]
62 fn spin(&mut self) {
63 for _ in 0..(1 << self.step.min(6)) {
64 core::hint::spin_loop();
65 }
66 if self.step <= 6 {
67 self.step += 1;
68 }
69 }
70}
71
72/// Node in the lock-free linked list.
73#[repr(C)]
74struct Node<K, V> {
75 retired: RetiredNode,
76 hash: u64,
77 key: K,
78 value: V,
79 next: Atomic<Node<K, V>>,
80}
81
82// SAFETY (kovan retirement rule): a retired Node's destructor may run on
83// any thread, and nodes (with K and V inside) move between threads — hence
84// `K: Send, V: Send` for Send. Unlike exclusive-transfer containers,
85// lookups DO produce `&K`/`&V` from a shared `&Node` (get() clones V
86// through &V under concurrent readers), so Sync additionally requires
87// `K: Sync, V: Sync` — the same bounds the map-level Sync impl below has
88// always required for sharing the map.
89unsafe impl<K: Send, V: Send> Send for Node<K, V> {}
90unsafe impl<K: Send + Sync, V: Send + Sync> Sync for Node<K, V> {}
91
92// ---------------------------------------------------------------------------
93// Harris-style logical deletion
94// ---------------------------------------------------------------------------
95//
96// Removing (or replacing) a node first TAGS the victim's `next` pointer
97// (low bit set) — the logical delete — and only then unlinks it from its
98// predecessor. The thread whose tag-CAS succeeded exclusively owns the node
99// and is the only one to `retire()` it. This closes two races a plain
100// unlink-CAS protocol has:
101//
102// * insert-after-removed-tail: a tail insert CASes `tail.next: null -> new`;
103// if the tail was concurrently unlinked and retired, the new node is
104// spliced onto dead memory — the insert is lost and the node leaks.
105// With tagging, the remover first turns the tail's `next` into
106// tagged-null, so the insert's CAS (expecting untagged null) fails.
107//
108// * adjacent removes: removing B (A->B->C) and C (B->C->D) concurrently
109// can unlink C from the already-detached B while C is still reachable
110// through A, retiring a reachable node (use-after-free for later
111// readers). With tagging, C's remover owns C via the tag; walkers
112// observe `B.next` tagged and never operate relative to deleted nodes.
113//
114// Invariants:
115// * tags appear only on `Node.next` fields, never on bucket heads
116// (snipping stores the untagged successor);
117// * a node whose `next` is tagged has been retired by its tag owner —
118// `clear()`, the migration sweep, and `Table::drop` must skip it;
119// * every traversal untags before following a `next` pointer.
120
121#[inline(always)]
122fn tagged<K, V>(p: *mut Node<K, V>) -> *mut Node<K, V> {
123 (p as usize | 1) as *mut Node<K, V>
124}
125
126#[inline(always)]
127fn untag<K, V>(p: *mut Node<K, V>) -> *mut Node<K, V> {
128 (p as usize & !1) as *mut Node<K, V>
129}
130
131#[inline(always)]
132fn is_tagged<K, V>(p: *const Node<K, V>) -> bool {
133 (p as usize) & 1 != 0
134}
135
136// ---------------------------------------------------------------------------
137// Single-allocation table: [TableHeader][Atomic<Node>; capacity]
138// ---------------------------------------------------------------------------
139//
140// The header and the bucket array share one allocation, so the read path is
141// `table ptr -> header line (mask, hot in cache) -> bucket line` — the same
142// number of cold dereferences as a fixed embedded array. The table pointer
143// itself is swapped atomically on resize.
144//
145// Reclamation goes through a tiny boxed `TableProxy` (RetiredNode at offset
146// 0, as `retire()` requires): retiring the proxy defers until every guard
147// that could observe the old table has been released; the proxy's destructor
148// then frees the table's remaining chains and the allocation itself.
149
150#[repr(C)]
151struct TableHeader {
152 mask: usize,
153 capacity: usize,
154}
155
156/// Borrowed view of a table allocation.
157struct TableRef<K: 'static, V: 'static> {
158 header: *mut TableHeader,
159 _marker: core::marker::PhantomData<(K, V)>,
160}
161
162impl<K, V> Clone for TableRef<K, V> {
163 fn clone(&self) -> Self {
164 *self
165 }
166}
167impl<K, V> Copy for TableRef<K, V> {}
168
169impl<K: 'static, V: 'static> TableRef<K, V> {
170 #[inline(always)]
171 fn from_raw(header: *mut TableHeader) -> Self {
172 Self {
173 header,
174 _marker: core::marker::PhantomData,
175 }
176 }
177
178 fn layout(capacity: usize) -> (core::alloc::Layout, usize) {
179 let header = core::alloc::Layout::new::<TableHeader>();
180 let buckets = core::alloc::Layout::array::<Atomic<Node<K, V>>>(capacity)
181 .expect("bucket array layout overflow");
182 let (layout, offset) = header.extend(buckets).expect("table layout overflow");
183 (layout.pad_to_align(), offset)
184 }
185
186 /// Allocate a zero-initialized table (`Atomic` buckets zero == null).
187 fn alloc(capacity: usize) -> Self {
188 let capacity = capacity.next_power_of_two().max(MIN_CAPACITY);
189 let (layout, _) = Self::layout(capacity);
190 // SAFETY: layout is non-zero sized; zeroed AtomicUsize == null bucket.
191 let header = unsafe { alloc::alloc::alloc_zeroed(layout) as *mut TableHeader };
192 assert!(!header.is_null(), "table allocation failed");
193 unsafe {
194 (*header).mask = capacity - 1;
195 (*header).capacity = capacity;
196 }
197 Self::from_raw(header)
198 }
199
200 /// Free remaining chains (skipping tagged nodes — already retired by
201 /// their tag owners) and the allocation itself.
202 ///
203 /// Free only the table allocation, not the chains (caller already drained
204 /// the live nodes, e.g. `IntoIter`). Tagged nodes remain kovan-owned.
205 ///
206 /// # Safety
207 /// Exclusive access; the chains must already be drained.
208 unsafe fn free_array_only(self) {
209 let capacity = unsafe { (*self.header).capacity };
210 let (layout, _) = Self::layout(capacity);
211 unsafe { alloc::alloc::dealloc(self.header as *mut u8, layout) };
212 }
213
214 /// # Safety
215 /// Caller must have exclusive access (map drop, or proxy reclamation
216 /// after guard quiescence).
217 unsafe fn free(self) {
218 let capacity = unsafe { (*self.header).capacity };
219 let guard = pin();
220 for i in 0..capacity {
221 let mut current = self.bucket(i).load(Ordering::Relaxed, &guard).as_raw();
222 while !current.is_null() {
223 unsafe {
224 let next = (*current).next.load(Ordering::Relaxed, &guard).as_raw();
225 if !is_tagged(next) {
226 drop(Box::from_raw(current));
227 }
228 current = untag(next);
229 }
230 }
231 }
232 drop(guard);
233 let (layout, _) = Self::layout(capacity);
234 unsafe { alloc::alloc::dealloc(self.header as *mut u8, layout) };
235 }
236
237 #[inline(always)]
238 fn as_raw(self) -> *mut TableHeader {
239 self.header
240 }
241
242 #[inline(always)]
243 fn capacity(self) -> usize {
244 unsafe { (*self.header).capacity }
245 }
246
247 #[inline(always)]
248 fn buckets(self) -> *const Atomic<Node<K, V>> {
249 let (_, offset) = Self::layout_offset();
250 unsafe { (self.header as *const u8).add(offset) as *const Atomic<Node<K, V>> }
251 }
252
253 /// Header/bucket offset is capacity-independent; compute it once.
254 #[inline(always)]
255 fn layout_offset() -> ((), usize) {
256 let header = core::alloc::Layout::new::<TableHeader>();
257 let one = core::alloc::Layout::new::<Atomic<Node<K, V>>>();
258 let (_, offset) = header.extend(one).expect("layout");
259 ((), offset)
260 }
261
262 #[inline(always)]
263 fn bucket_index(self, hash: u64) -> usize {
264 (hash as usize) & unsafe { (*self.header).mask }
265 }
266
267 #[inline(always)]
268 fn bucket(self, idx: usize) -> &'static Atomic<Node<K, V>> {
269 // SAFETY: idx is masked or bounded by capacity; the 'static is a
270 // lie scoped by the caller's guard (same discipline as Shared).
271 unsafe { &*self.buckets().add(idx) }
272 }
273}
274
275/// Reclamation proxy for a table allocation (RetiredNode at offset 0).
276#[repr(C)]
277struct TableProxy<K: 'static, V: 'static> {
278 retired: RetiredNode,
279 table: TableRef<K, V>,
280}
281
282// SAFETY (kovan retirement rule): the proxy's destructor (running on any
283// thread) frees the table's nodes, hence K, V: Send.
284unsafe impl<K: Send, V: Send> Send for TableProxy<K, V> {}
285unsafe impl<K: Send + Sync, V: Send + Sync> Sync for TableProxy<K, V> {}
286
287impl<K, V> Drop for TableProxy<K, V> {
288 fn drop(&mut self) {
289 // SAFETY: kovan reclaimed the proxy only after every guard that
290 // could observe the old table has been released.
291 unsafe { self.table.free() };
292 }
293}
294
295/// High-Performance Lock-Free Map with automatic grow/shrink.
296pub struct HashMap<K: 'static, V: 'static, S = FixedState> {
297 table: Atomic<TableHeader>,
298 /// Approximate live-entry count driving the resize thresholds.
299 /// Signed: a transient negative under racing removes is harmless and
300 /// avoids a CAS loop (fetch_update) on the hot remove path.
301 count: AtomicIsize,
302 /// Single-resizer latch; writers wait while a resize is in flight.
303 resizing: AtomicBool,
304 /// Shrink floor: the initial capacity. The map never shrinks below the
305 /// size it was created with, preserving the caller's sizing intent (and
306 /// the historical fixed-table behavior for `new()`).
307 floor: usize,
308 hasher: S,
309 _marker: core::marker::PhantomData<(K, V)>,
310}
311
312#[cfg(feature = "std")]
313impl<K, V> HashMap<K, V, FixedState>
314where
315 K: Hash + Eq + Clone + 'static,
316 V: Clone + 'static,
317{
318 /// Creates a new empty hash map with FoldHash (FixedState).
319 pub fn new() -> Self {
320 Self::with_hasher(FixedState::default())
321 }
322
323 /// Creates a new empty hash map with at least `capacity` buckets.
324 pub fn with_capacity(capacity: usize) -> Self {
325 Self::with_capacity_and_hasher(capacity, FixedState::default())
326 }
327}
328
329impl<K, V, S> HashMap<K, V, S>
330where
331 K: Hash + Eq + Clone + 'static,
332 V: Clone + 'static,
333 S: BuildHasher,
334{
335 /// Creates a new hash map with custom hasher.
336 pub fn with_hasher(hasher: S) -> Self {
337 Self::with_capacity_and_hasher(DEFAULT_CAPACITY, hasher)
338 }
339
340 /// Creates a new hash map with at least `capacity` buckets and a custom hasher.
341 ///
342 /// The map grows when its load factor exceeds 0.75 and shrinks when it
343 /// falls below 0.25 — but never below `capacity`.
344 pub fn with_capacity_and_hasher(capacity: usize, hasher: S) -> Self {
345 let table = TableRef::<K, V>::alloc(capacity);
346 let floor = table.capacity();
347 Self {
348 table: Atomic::new(table.as_raw()),
349 count: AtomicIsize::new(0),
350 resizing: AtomicBool::new(false),
351 floor,
352 hasher,
353 _marker: core::marker::PhantomData,
354 }
355 }
356
357 /// Returns the current number of buckets.
358 pub fn capacity(&self) -> usize {
359 let guard = pin();
360 let table = TableRef::<K, V>::from_raw(self.table.load(Ordering::Acquire, &guard).as_raw());
361 table.capacity()
362 }
363
364 /// Spin until any in-flight resize completes.
365 #[inline]
366 fn wait_for_resize(&self) {
367 while self.resizing.load(Ordering::Acquire) {
368 core::hint::spin_loop();
369 }
370 }
371
372 /// Optimized get operation. Never blocks — reads the current table
373 /// snapshot under a guard, even while a resize is in flight.
374 pub fn get<Q>(&self, key: &Q) -> Option<V>
375 where
376 K: Borrow<Q>,
377 Q: Hash + Eq + ?Sized,
378 {
379 let hash = self.hasher.hash_one(key);
380 let guard = pin();
381 let table = TableRef::<K, V>::from_raw(self.table.load(Ordering::Acquire, &guard).as_raw());
382 let bucket = table.bucket(table.bucket_index(hash));
383
384 let mut current = bucket.load(Ordering::Acquire, &guard).as_raw();
385 while !current.is_null() {
386 unsafe {
387 let node = &*current;
388 // Check hash first (integer compare is fast). Matching a
389 // logically-deleted node is linearizable (the read happened
390 // before the delete), so no tag check on the match path.
391 if node.hash == hash && node.key.borrow() == key {
392 return Some(node.value.clone());
393 }
394 // Untag: the pointer may carry the deletion tag.
395 current = untag(node.next.load(Ordering::Acquire, &guard).as_raw());
396 }
397 }
398 None
399 }
400
401 /// Checks if the key exists.
402 pub fn contains_key<Q>(&self, key: &Q) -> bool
403 where
404 K: Borrow<Q>,
405 Q: Hash + Eq + ?Sized,
406 {
407 self.get(key).is_some()
408 }
409
410 /// Insert a key-value pair.
411 pub fn insert(&self, key: K, value: V) -> Option<V> {
412 let hash = self.hasher.hash_one(&key);
413 let mut backoff = Backoff::new();
414 // Count a new key exactly once across re-validation retries
415 // (mirrors HopscotchMap: prevents both under-count, which causes
416 // cascading resizes, and double-count).
417 let mut counted = false;
418 // The first successful op's previous value is the linearized result;
419 // re-validation retries may replace a migrated clone of it.
420 let mut result: Option<Option<V>> = None;
421
422 'outer: loop {
423 self.wait_for_resize();
424
425 let guard = pin();
426 let table_raw = self.table.load(Ordering::Acquire, &guard).as_raw();
427 let table = TableRef::<K, V>::from_raw(table_raw);
428 if self.resizing.load(Ordering::Acquire) {
429 continue;
430 }
431
432 let bucket = table.bucket(table.bucket_index(hash));
433
434 // 1. Search for existing key to update (snip-walk: physically
435 // unlink logically-deleted nodes as we pass them).
436 let mut prev_link = bucket;
437 let mut current = prev_link.load(Ordering::Acquire, &guard).as_raw();
438
439 while !current.is_null() {
440 unsafe {
441 let node = &*current;
442 let next = node.next.load(Ordering::Acquire, &guard).as_raw();
443
444 if is_tagged(next) {
445 // Logically deleted: snip it out (its tag owner has
446 // already retired it). On contention restart the scan.
447 if prev_link
448 .compare_exchange(
449 Shared::from_raw(current),
450 Shared::from_raw(untag(next)),
451 Ordering::AcqRel,
452 Ordering::Relaxed,
453 &guard,
454 )
455 .is_err()
456 {
457 backoff.spin();
458 continue 'outer;
459 }
460 current = untag(next);
461 continue;
462 }
463
464 if node.hash == hash && node.key == key {
465 // Replace: logically delete the old node (tag-CAS
466 // makes us its exclusive owner), then swing the
467 // predecessor to the replacement in one step.
468 let old_value = node.value.clone();
469 if node
470 .next
471 .compare_exchange(
472 Shared::from_raw(next),
473 Shared::from_raw(tagged(next)),
474 Ordering::AcqRel,
475 Ordering::Relaxed,
476 &guard,
477 )
478 .is_err()
479 {
480 // Someone else deleted/replaced it first.
481 backoff.spin();
482 continue 'outer;
483 }
484 // We own the old node now — we retire it, exactly once.
485 let new_node = Box::into_raw(Box::new(Node {
486 retired: RetiredNode::new(),
487 hash,
488 key: key.clone(),
489 value: value.clone(),
490 next: Atomic::new(next),
491 }));
492 let swapped = prev_link
493 .compare_exchange(
494 Shared::from_raw(current),
495 Shared::from_raw(new_node),
496 Ordering::AcqRel,
497 Ordering::Relaxed,
498 &guard,
499 )
500 .is_ok();
501 // SAFETY: tag ownership; Node is #[repr(C)] with
502 // RetiredNode at offset 0.
503 retire(current);
504 if result.is_none() {
505 result = Some(Some(old_value));
506 }
507 if !swapped {
508 // A helper snipped the old node before our swing;
509 // the replacement is not installed — retry the
510 // whole op (the removal already linearized).
511 drop(Box::from_raw(new_node));
512 backoff.spin();
513 continue 'outer;
514 }
515 // Re-validate: if a resize started (or completed)
516 // since we loaded the table, the migration may have
517 // cloned the entry before our update — redo the op
518 // on the new table so the update is not lost.
519 if self.resizing.load(Ordering::SeqCst)
520 || self.table.load(Ordering::SeqCst, &guard).as_raw() != table_raw
521 {
522 continue 'outer;
523 }
524 return result.unwrap();
525 }
526
527 prev_link = &node.next;
528 current = next;
529 }
530 }
531
532 // 2. Key not found. Insert at TAIL (prev_link). The CAS expects
533 // an untagged null, so it fails if the tail node was
534 // concurrently logically deleted.
535 let new_node_ptr = Box::into_raw(Box::new(Node {
536 retired: RetiredNode::new(),
537 hash,
538 key: key.clone(),
539 value: value.clone(),
540 next: Atomic::null(),
541 }));
542
543 match prev_link.compare_exchange(
544 unsafe { Shared::from_raw(core::ptr::null_mut()) },
545 unsafe { Shared::from_raw(new_node_ptr) },
546 Ordering::Release,
547 Ordering::Relaxed,
548 &guard,
549 ) {
550 Ok(_) => {
551 if !counted {
552 counted = true;
553 self.count.fetch_add(1, Ordering::Relaxed);
554 }
555 if result.is_none() {
556 result = Some(None);
557 }
558 // Re-validate against a concurrent migration (see above).
559 if self.resizing.load(Ordering::SeqCst)
560 || self.table.load(Ordering::SeqCst, &guard).as_raw() != table_raw
561 {
562 continue 'outer;
563 }
564
565 // Grow check (only when we actually added an entry).
566 let new_count = self.count.load(Ordering::Relaxed).max(0) as usize;
567 let capacity = table.capacity();
568 // Integer load-factor check: count/cap > 3/4.
569 if 4 * new_count > 3 * capacity {
570 drop(guard);
571 self.try_resize(capacity * 2);
572 }
573 return result.unwrap();
574 }
575 Err(_) => {
576 // Contention at the tail — retry the search/append loop.
577 unsafe {
578 drop(Box::from_raw(new_node_ptr));
579 }
580 backoff.spin();
581 continue 'outer;
582 }
583 }
584 }
585 }
586
587 /// Insert a key-value pair only if the key does not exist.
588 /// Returns `None` if inserted, `Some(existing_value)` if the key already exists.
589 pub fn insert_if_absent(&self, key: K, value: V) -> Option<V> {
590 let hash = self.hasher.hash_one(&key);
591 let mut backoff = Backoff::new();
592 let mut counted = false;
593
594 'outer: loop {
595 self.wait_for_resize();
596
597 let guard = pin();
598 let table_raw = self.table.load(Ordering::Acquire, &guard).as_raw();
599 let table = TableRef::<K, V>::from_raw(table_raw);
600 if self.resizing.load(Ordering::Acquire) {
601 continue;
602 }
603
604 let bucket = table.bucket(table.bucket_index(hash));
605
606 // 1. Search for existing key (snip-walk).
607 let mut prev_link = bucket;
608 let mut current = prev_link.load(Ordering::Acquire, &guard).as_raw();
609
610 while !current.is_null() {
611 unsafe {
612 let node = &*current;
613 let next = node.next.load(Ordering::Acquire, &guard).as_raw();
614
615 if is_tagged(next) {
616 if prev_link
617 .compare_exchange(
618 Shared::from_raw(current),
619 Shared::from_raw(untag(next)),
620 Ordering::AcqRel,
621 Ordering::Relaxed,
622 &guard,
623 )
624 .is_err()
625 {
626 backoff.spin();
627 continue 'outer;
628 }
629 current = untag(next);
630 continue;
631 }
632
633 if node.hash == hash && node.key == key {
634 // Found on a retry. This may be our own migrated
635 // clone OR another caller's entry that landed while
636 // the table swapped - the two are indistinguishable
637 // here, and reporting None for someone else's entry
638 // would admit a second winner. Return the canonical
639 // value either way (for our own clone that is a
640 // clone of the value we just inserted), matching
641 // HopscotchMap's retry semantics: under a concurrent
642 // resize a successful insert may report
643 // Some(its own value); callers must treat the
644 // returned value as canonical.
645 return Some(node.value.clone());
646 }
647 prev_link = &node.next;
648 current = next;
649 }
650 }
651
652 // 2. Key not found (or our pre-migration insert was not carried
653 // over) — insert at TAIL. Untagged-null expectation makes the
654 // CAS fail if the tail was concurrently logically deleted.
655 let new_node_ptr = Box::into_raw(Box::new(Node {
656 retired: RetiredNode::new(),
657 hash,
658 key: key.clone(),
659 value: value.clone(),
660 next: Atomic::null(),
661 }));
662
663 match prev_link.compare_exchange(
664 unsafe { Shared::from_raw(core::ptr::null_mut()) },
665 unsafe { Shared::from_raw(new_node_ptr) },
666 Ordering::Release,
667 Ordering::Relaxed,
668 &guard,
669 ) {
670 Ok(_) => {
671 if !counted {
672 counted = true;
673 self.count.fetch_add(1, Ordering::Relaxed);
674 }
675 // Re-validate against a concurrent migration.
676 if self.resizing.load(Ordering::SeqCst)
677 || self.table.load(Ordering::SeqCst, &guard).as_raw() != table_raw
678 {
679 continue 'outer;
680 }
681
682 let new_count = self.count.load(Ordering::Relaxed).max(0) as usize;
683 let capacity = table.capacity();
684 // Integer load-factor check: count/cap > 3/4.
685 if 4 * new_count > 3 * capacity {
686 drop(guard);
687 self.try_resize(capacity * 2);
688 }
689 return None;
690 }
691 Err(actual_val) => {
692 // Contention at the tail.
693 unsafe {
694 let appended_ptr = actual_val.as_raw();
695 drop(Box::from_raw(new_node_ptr));
696 if !is_tagged(appended_ptr) && !appended_ptr.is_null() {
697 let appended = &*appended_ptr;
698 if appended.hash == hash && appended.key == key {
699 // Race lost, key exists now. Same canonical-
700 // value rule as the retry-found path above:
701 // even if a pre-migration attempt of ours
702 // inserted, the surviving entry is what
703 // every caller must converge on.
704 return Some(appended.value.clone());
705 }
706 }
707 }
708 backoff.spin();
709 continue 'outer;
710 }
711 }
712 }
713 }
714
715 /// Returns the value corresponding to the key, or inserts the given value if the key is not present.
716 ///
717 /// This is linearizable: concurrent callers for the same key are guaranteed to
718 /// agree on which value was inserted (exactly one thread's CAS succeeds at the
719 /// list tail, and all others see that node on retry).
720 pub fn get_or_insert(&self, key: K, value: V) -> V {
721 match self.insert_if_absent(key, value.clone()) {
722 Some(existing) => existing,
723 None => value,
724 }
725 }
726
727 /// Remove a key-value pair.
728 pub fn remove<Q>(&self, key: &Q) -> Option<V>
729 where
730 K: Borrow<Q>,
731 Q: Hash + Eq + ?Sized,
732 {
733 let hash = self.hasher.hash_one(key);
734 let mut backoff = Backoff::new();
735 // First successful removal's value is the linearized result;
736 // re-validation retries only evict migrated clones.
737 let mut result: Option<V> = None;
738
739 'outer: loop {
740 self.wait_for_resize();
741
742 let guard = pin();
743 let table_raw = self.table.load(Ordering::Acquire, &guard).as_raw();
744 let table = TableRef::<K, V>::from_raw(table_raw);
745 if self.resizing.load(Ordering::Acquire) {
746 continue;
747 }
748
749 let bucket = table.bucket(table.bucket_index(hash));
750
751 let mut prev_link = bucket;
752 let mut current = prev_link.load(Ordering::Acquire, &guard).as_raw();
753
754 while !current.is_null() {
755 unsafe {
756 let node = &*current;
757 let next = node.next.load(Ordering::Acquire, &guard).as_raw();
758
759 if is_tagged(next) {
760 // Logically deleted by someone else: snip and move on.
761 if prev_link
762 .compare_exchange(
763 Shared::from_raw(current),
764 Shared::from_raw(untag(next)),
765 Ordering::AcqRel,
766 Ordering::Relaxed,
767 &guard,
768 )
769 .is_err()
770 {
771 backoff.spin();
772 continue 'outer;
773 }
774 current = untag(next);
775 continue;
776 }
777
778 if node.hash == hash && node.key.borrow() == key {
779 let old_value = node.value.clone();
780
781 // Logical delete: tag the victim's next. The tag
782 // owner — and only the tag owner — retires the node,
783 // and the tag makes concurrent tail-inserts onto
784 // this node fail.
785 if node
786 .next
787 .compare_exchange(
788 Shared::from_raw(next),
789 Shared::from_raw(tagged(next)),
790 Ordering::AcqRel,
791 Ordering::Relaxed,
792 &guard,
793 )
794 .is_err()
795 {
796 backoff.spin();
797 continue 'outer;
798 }
799
800 // Physical unlink (best effort — if it fails, a
801 // later walker snips it).
802 let _ = prev_link.compare_exchange(
803 Shared::from_raw(current),
804 Shared::from_raw(next),
805 Ordering::AcqRel,
806 Ordering::Relaxed,
807 &guard,
808 );
809
810 // SAFETY: tag ownership; Node is #[repr(C)] with
811 // RetiredNode at offset 0.
812 retire(current);
813 if result.is_none() {
814 result = Some(old_value);
815 }
816
817 // Single atomic decrement (signed counter — cannot
818 // wrap; a transient negative just clamps to 0 below).
819 let new_count =
820 (self.count.fetch_sub(1, Ordering::Relaxed) - 1).max(0) as usize;
821 // Integer load-factor check: count/cap < 1/4.
822 let shrink_to = (4 * new_count < table.capacity()
823 && table.capacity() > self.floor)
824 .then_some(table.capacity() / 2);
825
826 // Re-validate: a concurrent migration may have cloned
827 // this entry into the new table before we deleted it
828 // here — redo the removal on the current table so the
829 // key does not resurrect.
830 if self.resizing.load(Ordering::SeqCst)
831 || self.table.load(Ordering::SeqCst, &guard).as_raw() != table_raw
832 {
833 continue 'outer;
834 }
835
836 if let Some(cap) = shrink_to {
837 drop(guard);
838 self.try_resize(cap);
839 }
840 return result;
841 }
842
843 prev_link = &node.next;
844 current = next;
845 }
846 }
847
848 // Key not present in the current table.
849 return result;
850 }
851 }
852
853 /// Remove **all** nodes matching `key`, returning the most recent value
854 /// if the key was present.
855 ///
856 /// [`remove`](Self::remove) unlinks only the first matching entry.
857 /// Insert/remove races can transiently leave more than one entry for
858 /// the same key ("versions"); after a plain `remove()` an older version
859 /// would become visible again. This method keeps removing until a full
860 /// scan finds no match, so the key is guaranteed absent at the
861 /// linearization point of the final scan.
862 ///
863 /// Use `remove()` for single-version removal semantics and
864 /// `force_remove()` when the key must be fully evicted.
865 ///
866 /// Note: a concurrent `insert` of the same key can land after the final
867 /// scan, as with any removal under contention.
868 pub fn force_remove<Q>(&self, key: &Q) -> Option<V>
869 where
870 K: Borrow<Q>,
871 Q: Hash + Eq + ?Sized,
872 {
873 let mut newest = None;
874 loop {
875 match self.remove(key) {
876 Some(v) => {
877 // The first removal unlinks the first match in scan
878 // order — the live (most recent) version.
879 if newest.is_none() {
880 newest = Some(v);
881 }
882 }
883 None => return newest,
884 }
885 }
886 }
887
888 /// Clear the map.
889 pub fn clear(&self) {
890 // Take the resize latch so the table cannot be swapped (and no
891 // writer is mid-migration) while we unlink the chains.
892 while self
893 .resizing
894 .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
895 .is_err()
896 {
897 core::hint::spin_loop();
898 }
899
900 let guard = pin();
901 let table = TableRef::<K, V>::from_raw(self.table.load(Ordering::Acquire, &guard).as_raw());
902
903 for i in 0..table.capacity() {
904 let bucket = table.bucket(i);
905 loop {
906 let head = bucket.load(Ordering::Acquire, &guard);
907 if head.is_null() {
908 break;
909 }
910
911 // Try to unlink the whole chain at once
912 match bucket.compare_exchange(
913 head,
914 unsafe { Shared::from_raw(core::ptr::null_mut()) },
915 Ordering::Release,
916 Ordering::Relaxed,
917 &guard,
918 ) {
919 Ok(_) => {
920 // Retire the chain's live nodes. Tagged nodes were
921 // already retired by their tag owners — skip them.
922 unsafe {
923 let mut current = head.as_raw();
924 while !current.is_null() {
925 let next = (*current).next.load(Ordering::Relaxed, &guard).as_raw();
926 if !is_tagged(next) {
927 // SAFETY: allocated via Box::into_raw;
928 // Node is #[repr(C)], RetiredNode first.
929 retire(current);
930 }
931 current = untag(next);
932 }
933 }
934 break;
935 }
936 Err(_) => {
937 // Contention, retry
938 continue;
939 }
940 }
941 }
942 }
943
944 self.count.store(0, Ordering::Release);
945 self.resizing.store(false, Ordering::Release);
946 }
947
948 /// Returns true if the map is empty.
949 pub fn is_empty(&self) -> bool {
950 self.len() == 0
951 }
952
953 /// Returns the number of elements in the map.
954 ///
955 /// O(1): maintained by insert/remove. Approximate while concurrent
956 /// updates are in flight (exact in quiescence), like `HopscotchMap`.
957 pub fn len(&self) -> usize {
958 self.count.load(Ordering::Relaxed).max(0) as usize
959 }
960
961 /// Resize the table to `new_capacity` buckets (single resizer wins).
962 ///
963 /// Clones every entry into a new table, swaps the table pointer, then
964 /// retires the old table. The old table's destructor frees whatever
965 /// nodes remain in its chains at reclamation time — entries removed or
966 /// replaced in the meantime were unlinked and retired individually, so
967 /// nothing is freed twice and nothing leaks.
968 fn try_resize(&self, new_capacity: usize) {
969 if self
970 .resizing
971 .compare_exchange(false, true, Ordering::SeqCst, Ordering::Relaxed)
972 .is_err()
973 {
974 return;
975 }
976
977 let new_capacity = new_capacity
978 .next_power_of_two()
979 .max(MIN_CAPACITY)
980 .max(self.floor);
981 let guard = pin();
982 let old_raw = self.table.load(Ordering::Acquire, &guard).as_raw();
983 let old_table = TableRef::<K, V>::from_raw(old_raw);
984
985 if old_table.capacity() == new_capacity {
986 self.resizing.store(false, Ordering::Release);
987 return;
988 }
989
990 let new_table = TableRef::<K, V>::alloc(new_capacity);
991
992 // Migrate: clone every live entry (logically-deleted nodes — tagged
993 // next — are skipped). We are the only writer of the new table (it
994 // is unpublished), so plain stores are sufficient.
995 for i in 0..old_table.capacity() {
996 let bucket = old_table.bucket(i);
997 let mut current = bucket.load(Ordering::Acquire, &guard).as_raw();
998 while !current.is_null() {
999 let node = unsafe { &*current };
1000 let next = node.next.load(Ordering::Acquire, &guard).as_raw();
1001 if !is_tagged(next) {
1002 let dst = new_table.bucket(new_table.bucket_index(node.hash));
1003 let head = dst.load(Ordering::Relaxed, &guard);
1004 let clone = Box::into_raw(Box::new(Node {
1005 retired: RetiredNode::new(),
1006 hash: node.hash,
1007 key: node.key.clone(),
1008 value: node.value.clone(),
1009 next: Atomic::new(head.as_raw()),
1010 }));
1011 dst.store(unsafe { Shared::from_raw(clone) }, Ordering::Relaxed);
1012 }
1013 current = untag(next);
1014 }
1015 }
1016
1017 match self.table.compare_exchange(
1018 unsafe { Shared::from_raw(old_raw) },
1019 unsafe { Shared::from_raw(new_table.as_raw()) },
1020 Ordering::Release,
1021 Ordering::Relaxed,
1022 &guard,
1023 ) {
1024 Ok(_) => {
1025 // Retire the old table through a proxy: reclamation (which
1026 // frees the remaining chains and the allocation) is deferred
1027 // until every guard that could observe it is gone.
1028 let proxy = Box::into_raw(Box::new(TableProxy {
1029 retired: RetiredNode::new(),
1030 table: old_table,
1031 }));
1032 // SAFETY: TableProxy is #[repr(C)] with RetiredNode at
1033 // offset 0, allocated via Box::into_raw.
1034 unsafe { retire(proxy) };
1035 }
1036 Err(_) => {
1037 // Table changed under us (cannot normally happen — we hold
1038 // the resize latch). Discard the unpublished new table.
1039 unsafe { new_table.free() };
1040 }
1041 }
1042
1043 self.resizing.store(false, Ordering::Release);
1044 }
1045
1046 /// Returns an iterator over the map entries.
1047 /// Yields (K, V) clones from a table snapshot taken at creation.
1048 pub fn iter(&self) -> Iter<'_, K, V, S> {
1049 let guard = pin();
1050 let table = TableRef::<K, V>::from_raw(self.table.load(Ordering::Acquire, &guard).as_raw());
1051 Iter {
1052 _map: self,
1053 table,
1054 bucket_idx: 0,
1055 current: core::ptr::null(),
1056 guard,
1057 }
1058 }
1059
1060 /// Returns an iterator over the map keys.
1061 /// Yields K clones.
1062 pub fn keys(&self) -> Keys<'_, K, V, S> {
1063 Keys { iter: self.iter() }
1064 }
1065
1066 /// Returns an iterator over the map values (clones `V`).
1067 pub fn values(&self) -> Values<'_, K, V, S> {
1068 Values { iter: self.iter() }
1069 }
1070
1071 /// Insert all `(K, V)` pairs from `iter`. Takes `&self` (concurrent map).
1072 pub fn extend<I: IntoIterator<Item = (K, V)>>(&self, iter: I) {
1073 for (k, v) in iter {
1074 self.insert(k, v);
1075 }
1076 }
1077
1078 /// Get the underlying hasher itself.
1079 pub fn hasher(&self) -> &S {
1080 &self.hasher
1081 }
1082}
1083
1084/// Iterator over HashMap entries.
1085///
1086/// Field ordering matters for drop safety.
1087/// Rust drops struct fields in declaration order.
1088/// The `guard` must be dropped *after* `current`/`table` so that the epoch
1089/// pin covering the snapshot is not released before we're done with the raw
1090/// pointers.
1091pub struct Iter<'a, K: 'static, V: 'static, S> {
1092 _map: &'a HashMap<K, V, S>,
1093 table: TableRef<K, V>,
1094 bucket_idx: usize,
1095 current: *const Node<K, V>,
1096 guard: kovan::Guard,
1097}
1098
1099impl<'a, K, V, S> Iterator for Iter<'a, K, V, S>
1100where
1101 K: Clone,
1102 V: Clone,
1103{
1104 type Item = (K, V);
1105
1106 fn next(&mut self) -> Option<Self::Item> {
1107 loop {
1108 if !self.current.is_null() {
1109 unsafe {
1110 let node = &*self.current;
1111 let next = node.next.load(Ordering::Acquire, &self.guard).as_raw();
1112 // Advance current (the pointer may carry a deletion tag).
1113 self.current = untag(next);
1114 if is_tagged(next) {
1115 // Logically deleted — do not yield.
1116 continue;
1117 }
1118 return Some((node.key.clone(), node.value.clone()));
1119 }
1120 }
1121
1122 // Move to next bucket
1123 let table = self.table;
1124 if self.bucket_idx >= table.capacity() {
1125 return None;
1126 }
1127
1128 let bucket = table.bucket(self.bucket_idx);
1129 self.bucket_idx += 1;
1130 self.current = bucket.load(Ordering::Acquire, &self.guard).as_raw();
1131 }
1132 }
1133}
1134
1135/// Iterator over HashMap keys.
1136pub struct Keys<'a, K: 'static, V: 'static, S> {
1137 iter: Iter<'a, K, V, S>,
1138}
1139
1140impl<'a, K, V, S> Iterator for Keys<'a, K, V, S>
1141where
1142 K: Clone,
1143 V: Clone,
1144{
1145 type Item = K;
1146
1147 fn next(&mut self) -> Option<Self::Item> {
1148 self.iter.next().map(|(k, _)| k)
1149 }
1150}
1151
1152impl<'a, K, V, S> IntoIterator for &'a HashMap<K, V, S>
1153where
1154 K: Hash + Eq + Clone + 'static,
1155 V: Clone + 'static,
1156 S: BuildHasher,
1157{
1158 type Item = (K, V);
1159 type IntoIter = Iter<'a, K, V, S>;
1160
1161 fn into_iter(self) -> Self::IntoIter {
1162 self.iter()
1163 }
1164}
1165
1166/// Iterator over HashMap values (clones `V`).
1167pub struct Values<'a, K: 'static, V: 'static, S> {
1168 iter: Iter<'a, K, V, S>,
1169}
1170
1171impl<'a, K, V, S> Iterator for Values<'a, K, V, S>
1172where
1173 K: Clone,
1174 V: Clone,
1175{
1176 type Item = V;
1177
1178 #[inline]
1179 fn next(&mut self) -> Option<V> {
1180 self.iter.next().map(|(_, v)| v)
1181 }
1182}
1183
1184/// Owned iterator yielding `(K, V)` by value — moves out of the nodes, no
1185/// clone. Consuming the map gives exclusive access, so no guard protection of
1186/// the yielded values is needed.
1187pub struct IntoIter<K: 'static, V: 'static> {
1188 table: TableRef<K, V>,
1189 bucket_idx: usize,
1190 current: *mut Node<K, V>,
1191 guard: kovan::Guard,
1192}
1193
1194impl<K, V> Iterator for IntoIter<K, V> {
1195 type Item = (K, V);
1196
1197 fn next(&mut self) -> Option<(K, V)> {
1198 loop {
1199 if !self.current.is_null() {
1200 let node = self.current;
1201 let next = unsafe { (*node).next.load(Ordering::Acquire, &self.guard).as_raw() };
1202 self.current = untag(next);
1203 if is_tagged(next) {
1204 continue; // logically deleted, owned by kovan
1205 }
1206 // Move K and V out, then free the shell without running drop.
1207 let k = unsafe { core::ptr::read(&(*node).key) };
1208 let v = unsafe { core::ptr::read(&(*node).value) };
1209 unsafe {
1210 alloc::alloc::dealloc(
1211 node as *mut u8,
1212 core::alloc::Layout::new::<Node<K, V>>(),
1213 );
1214 }
1215 return Some((k, v));
1216 }
1217 if self.bucket_idx >= self.table.capacity() {
1218 return None;
1219 }
1220 let bucket = self.table.bucket(self.bucket_idx);
1221 self.bucket_idx += 1;
1222 self.current = bucket.load(Ordering::Acquire, &self.guard).as_raw();
1223 }
1224 }
1225}
1226
1227impl<K, V> Drop for IntoIter<K, V> {
1228 fn drop(&mut self) {
1229 while self.next().is_some() {} // drop remaining live K/V + free shells
1230 unsafe { self.table.free_array_only() };
1231 }
1232}
1233
1234impl<K, V, S> IntoIterator for HashMap<K, V, S>
1235where
1236 K: 'static,
1237 V: 'static,
1238{
1239 type Item = (K, V);
1240 type IntoIter = IntoIter<K, V>;
1241
1242 fn into_iter(self) -> IntoIter<K, V> {
1243 let mut me = core::mem::ManuallyDrop::new(self);
1244 let guard = pin();
1245 let table = TableRef::<K, V>::from_raw(me.table.load(Ordering::Relaxed, &guard).as_raw());
1246 // Suppress HashMap::drop (we own the table now); drop only the hasher —
1247 // the remaining fields are atomics / usize / ZST marker.
1248 unsafe { core::ptr::drop_in_place(&mut me.hasher) };
1249 IntoIter {
1250 table,
1251 bucket_idx: 0,
1252 current: core::ptr::null_mut(),
1253 guard,
1254 }
1255 }
1256}
1257
1258impl<K, V, S> core::iter::FromIterator<(K, V)> for HashMap<K, V, S>
1259where
1260 K: Hash + Eq + Clone + Send + 'static,
1261 V: Clone + Send + 'static,
1262 S: BuildHasher + Default,
1263{
1264 fn from_iter<I: IntoIterator<Item = (K, V)>>(iter: I) -> Self {
1265 let map = Self::with_capacity_and_hasher(MIN_CAPACITY, S::default());
1266 for (k, v) in iter {
1267 map.insert(k, v);
1268 }
1269 map
1270 }
1271}
1272
1273#[cfg(feature = "std")]
1274impl<K, V> Default for HashMap<K, V, FixedState>
1275where
1276 K: Hash + Eq + Clone + 'static,
1277 V: Clone + 'static,
1278{
1279 fn default() -> Self {
1280 Self::new()
1281 }
1282}
1283
1284// SAFETY: HashMap is Send if K, V, S are Send (moving ownership between threads).
1285// HashMap is Sync if K, V, S are Send+Sync. The stronger bound on Sync is needed
1286// because concurrent `get()` calls clone V through a `&V` reference across threads;
1287// if V were Send but not Sync, sharing `&HashMap` could transmit a non-Sync V reference
1288// to another thread via clone(), violating thread-safety.
1289unsafe impl<K: Send, V: Send, S: Send> Send for HashMap<K, V, S> {}
1290unsafe impl<K: Send + Sync, V: Send + Sync, S: Send + Sync> Sync for HashMap<K, V, S> {}
1291
1292impl<K: 'static, V: 'static, S> Drop for HashMap<K, V, S> {
1293 fn drop(&mut self) {
1294 // SAFETY: `drop(&mut self)` guarantees exclusive ownership — no concurrent
1295 // readers can exist. Rust's type system enforces this: `Iter<'a, …>` borrows
1296 // `&'a HashMap`, so it cannot outlive the `HashMap`. The Table's destructor
1297 // frees its chains.
1298 let guard = pin();
1299 let table = TableRef::<K, V>::from_raw(self.table.load(Ordering::Relaxed, &guard).as_raw());
1300 drop(guard);
1301 // SAFETY: exclusive access; frees remaining chains + the allocation.
1302 unsafe { table.free() };
1303
1304 // Flush nodes/tables previously retired by concurrent operations
1305 kovan::flush();
1306 }
1307}
1308
1309#[cfg(test)]
1310mod tests {
1311 use super::*;
1312
1313 #[test]
1314 fn test_insert_and_get() {
1315 let map = HashMap::new();
1316 assert_eq!(map.insert(1, 100), None);
1317 assert_eq!(map.get(&1), Some(100));
1318 assert_eq!(map.get(&2), None);
1319 }
1320
1321 #[test]
1322 fn test_insert_replace() {
1323 let map = HashMap::new();
1324 assert_eq!(map.insert(1, 100), None);
1325 assert_eq!(map.insert(1, 200), Some(100));
1326 assert_eq!(map.get(&1), Some(200));
1327 }
1328
1329 #[test]
1330 fn test_grow() {
1331 let map = HashMap::with_capacity(64);
1332 assert_eq!(map.capacity(), 64);
1333 for i in 0..1000u64 {
1334 map.insert(i, i * 2);
1335 }
1336 assert!(map.capacity() > 64, "map should have grown");
1337 for i in 0..1000u64 {
1338 assert_eq!(map.get(&i), Some(i * 2));
1339 }
1340 assert_eq!(map.len(), 1000);
1341 }
1342
1343 #[test]
1344 fn test_shrink() {
1345 let map = HashMap::with_capacity(64);
1346 for i in 0..1000u64 {
1347 map.insert(i, i);
1348 }
1349 let grown = map.capacity();
1350 assert!(grown > 64);
1351 for i in 0..1000u64 {
1352 map.remove(&i);
1353 }
1354 assert!(
1355 map.capacity() < grown,
1356 "map should have shrunk (capacity {} -> {})",
1357 grown,
1358 map.capacity()
1359 );
1360 assert!(map.capacity() >= 64, "never below the initial capacity");
1361 assert_eq!(map.len(), 0);
1362 }
1363
1364 #[test]
1365 fn test_no_shrink_below_floor() {
1366 let map = HashMap::with_capacity(4096);
1367 for i in 0..100u64 {
1368 map.insert(i, i);
1369 }
1370 for i in 0..100u64 {
1371 map.remove(&i);
1372 }
1373 assert_eq!(map.capacity(), 4096, "floor preserves sizing intent");
1374 }
1375
1376 #[test]
1377 fn test_concurrent_inserts() {
1378 use alloc::sync::Arc;
1379 extern crate std;
1380 use std::thread;
1381
1382 let map = Arc::new(HashMap::new());
1383 let mut handles = alloc::vec::Vec::new();
1384
1385 for thread_id in 0..4 {
1386 let map_clone = Arc::clone(&map);
1387 let handle = thread::spawn(move || {
1388 for i in 0..1000 {
1389 let key = thread_id * 1000 + i;
1390 map_clone.insert(key, key * 2);
1391 }
1392 });
1393 handles.push(handle);
1394 }
1395
1396 for handle in handles {
1397 handle.join().unwrap();
1398 }
1399
1400 for thread_id in 0..4 {
1401 for i in 0..1000 {
1402 let key = thread_id * 1000 + i;
1403 assert_eq!(map.get(&key), Some(key * 2));
1404 }
1405 }
1406 }
1407
1408 #[test]
1409 fn test_concurrent_grow() {
1410 use alloc::sync::Arc;
1411 extern crate std;
1412 use std::thread;
1413
1414 let map = Arc::new(HashMap::with_capacity(64));
1415 let mut handles = alloc::vec::Vec::new();
1416
1417 for thread_id in 0..8u64 {
1418 let map_clone = Arc::clone(&map);
1419 handles.push(thread::spawn(move || {
1420 for i in 0..2000u64 {
1421 let key = thread_id * 10_000 + i;
1422 map_clone.insert(key, key);
1423 }
1424 }));
1425 }
1426 for handle in handles {
1427 handle.join().unwrap();
1428 }
1429
1430 for thread_id in 0..8u64 {
1431 for i in 0..2000u64 {
1432 let key = thread_id * 10_000 + i;
1433 assert_eq!(map.get(&key), Some(key), "lost key {key} during growth");
1434 }
1435 }
1436 assert!(map.capacity() >= 16_000);
1437 }
1438}