kevy_map/map_keyed.rs
1//! Insert, grow, look up and remove — the operations that need to compare keys.
2//!
3//! The layout they operate on is documented in [`crate::map`]: `cap` metadata
4//! bytes plus a `GROUP_WIDTH` mirror tail, one byte per slot, `EMPTY` = 0xFF,
5//! `DELETED` = 0x80, and a full slot holding `h2(hash)` — the top seven bits,
6//! never 0x80 or 0xFF. This file is the probe that reads them.
7//!
8//! # The probe
9//!
10//! Probing is by **group of 16, then linear**, not by the more usual
11//! quadratic step:
12//!
13//! ```text
14//! group_start = hash & mask
15//! loop:
16//! load 16 metadata bytes at group_start (one SIMD word)
17//! for each byte == h2(hash): compare the key (a real candidate)
18//! if any byte == EMPTY: stop (the key is not here)
19//! group_start = (group_start + 16) & mask (next group, linear)
20//! ```
21//!
22//! Linear beats quadratic here because the scan is already group-aware: at
23//! this load factor the next group is usually the next cache line, and a
24//! quadratic step throws that away for a collision pattern the h2 filter has
25//! already broken up.
26//!
27//! # Three invariants the probe depends on
28//!
29//! 1. **`EMPTY` terminates, `DELETED` does not.** That is the whole reason
30//! the two constants differ: a removed slot must stay walkable or every
31//! key that probed past it becomes unreachable. Removal writes `DELETED`,
32//! never `EMPTY`.
33//! 2. **The slot is marked `DELETED` *before* the value is moved out.**
34//! `remove` writes the metadata byte first, then `ptr::read`s the pair.
35//! The order is what makes the move sound: once the byte says `DELETED`,
36//! nothing — not a later probe, not `Drop`, not a rehash — will read that
37//! slot again, so moving the `(K, V)` out cannot become a double drop.
38//! Reading first and marking second would leave a window in which the slot
39//! claims to hold a value that has already been moved away.
40//! 3. **The mirror tail is kept in sync.** Every metadata write goes to both
41//! `i` and its mirror index, so a group load starting near the end of the
42//! table reads real bytes rather than falling off the allocation.
43//!
44//! # Growth
45//!
46//! Growth rebuilds rather than rehashes in place, and reinserts through
47//! `insert_known_unique`, which skips the key comparison entirely — the old
48//! table already proved every key distinct. That turns the rehash into one
49//! `match_byte(EMPTY)` per key.
50
51use core::borrow::Borrow;
52use core::ptr;
53
54use kevy_hash::KevyHash;
55
56use crate::group::Group;
57use crate::map::{DELETED, EMPTY, GROUP_WIDTH, KevyMap, MIN_CAP, ProbeOutcome, h2};
58
59impl<K: KevyHash + Eq, V> KevyMap<K, V> {
60 /// Insert `(key, value)`. Returns the old value if `key` was already
61 /// present. Following `std::HashMap` semantics, the existing K is kept on
62 /// overwrite — only V is replaced.
63 /// # Examples
64 ///
65 /// ```
66 /// let mut m = kevy_map::KevyMap::new();
67 /// assert_eq!(m.insert(b"k".to_vec(), 1u32), None, "no previous value");
68 /// assert_eq!(m.insert(b"k".to_vec(), 2), Some(1), "the OLD value comes back");
69 /// assert_eq!(m.get(b"k".as_slice()), Some(&2));
70 /// ```
71 pub fn insert(&mut self, key: K, value: V) -> Option<V> {
72 self.maybe_grow();
73 let hash = key.kevy_hash();
74 match self.probe_with_key(hash, &key) {
75 ProbeOutcome::Found(idx) => {
76 // SAFETY: slot is full ⇒ initialised. We replace only the V
77 // field; the old K is kept (std HashMap semantics).
78 // SAFETY: `idx` came from a probe that found a full metadata byte, so the
79 // slot at `idx` holds an initialised `(K, V)` inside the slot allocation.
80 let v_ptr = unsafe {
81 let kv: *mut (K, V) = self.slots_ptr.as_ptr().add(idx).cast::<(K, V)>();
82 ptr::addr_of_mut!((*kv).1)
83 };
84 // SAFETY: `v_ptr` points at that slot's initialised `V`, so the old value
85 // is a valid `V` to move out and the new one is written in its place.
86 let old_v = unsafe { ptr::replace(v_ptr, value) };
87 drop(key);
88 Some(old_v)
89 }
90 ProbeOutcome::NotFound { insert_at, via_tombstone } => {
91 self.set_meta(insert_at, h2(hash));
92 // SAFETY: insert_at < cap ⇒ slot pointer in-bounds; we write
93 // (K, V) into a previously uninitialised slot.
94 unsafe {
95 (*self.slots_ptr.as_ptr().add(insert_at)).write((key, value));
96 }
97 self.occupied += 1;
98 if via_tombstone {
99 self.deleted -= 1;
100 }
101 None
102 }
103 }
104 }
105
106 pub(crate) fn maybe_grow(&mut self) {
107 if self.cap == 0 || (self.occupied + self.deleted) >= self.threshold() {
108 self.grow();
109 }
110 }
111
112 fn grow(&mut self) {
113 let new_cap = if self.cap == 0 {
114 MIN_CAP
115 } else {
116 self.cap
117 .checked_mul(2)
118 .expect("a capacity that overflows usize could not have been allocated")
119 };
120 let mut new_table = Self::alloc_table(new_cap);
121 // Move every live entry over. After ptr::read'ing a slot we mark its
122 // metadata DELETED, so any subsequent Drop (incl. panic unwind) won't
123 // double-free; the old allocation will free with all-DELETED metadata.
124 //
125 // Only iterate the real slot range `[0, cap)`; the trailing mirror
126 // bytes are bookkeeping for SIMD-load wraparound, not real slots.
127 // Direct metadata writes are safe here because the old `self` table
128 // is going away (we swap with new_table then drop), so a stale mirror
129 // doesn't matter.
130 let old_cap = self.cap;
131 for i in 0..old_cap {
132 // SAFETY: i < old_cap ⇒ metadata in-bounds.
133 let meta = unsafe { *self.metadata_ptr.as_ptr().add(i) };
134 if meta & 0x80 == 0 {
135 // SAFETY: full slot ⇒ initialised; we mark DELETED immediately
136 // so this byte is never re-read as occupied.
137 let (k, v) = unsafe { ptr::read(self.slots_ptr.as_ptr().add(i) as *const (K, V)) };
138 // SAFETY: `i < cap`, so this is inside the metadata range. Writing DELETED
139 // immediately is what keeps the `ptr::read` above from being a double move:
140 // the byte is never seen as occupied again.
141 unsafe { *self.metadata_ptr.as_ptr().add(i) = DELETED };
142 let hash = k.kevy_hash();
143 new_table.insert_known_unique(hash, k, v);
144 }
145 }
146 // All occupied entries are now in new_table; the old self has no live slots.
147 self.occupied = 0;
148 self.deleted = 0;
149 core::mem::swap(self, &mut new_table);
150 // new_table (now the old self) drops; metadata is all DELETED (or EMPTY
151 // for previously-empty slots) ⇒ Drop walks but touches no slots.
152 }
153
154 /// Insert under the assumption that the key isn't already present (used
155 /// by `grow` to repopulate the new table). Skips the duplicate-key
156 /// check. Uses a 16-slot SIMD group scan to find the first EMPTY.
157 fn insert_known_unique(&mut self, hash: u64, k: K, v: V) {
158 let h2v = h2(hash);
159 let mut group_start = (hash as usize) & self.mask;
160 loop {
161 // SAFETY: metadata is `cap + GROUP_WIDTH` bytes; group_start
162 // is in `[0, cap)`; the load reads 16 bytes which lie inside the
163 // buffer thanks to the mirror tail.
164 let g = unsafe { Group::load(self.metadata_ptr.as_ptr().add(group_start)) };
165 if let Some(m) = g.match_byte(EMPTY).lowest_set() {
166 let slot = (group_start + m) & self.mask;
167 self.set_meta(slot, h2v);
168 // SAFETY: slot < cap.
169 unsafe {
170 (*self.slots_ptr.as_ptr().add(slot)).write((k, v));
171 }
172 self.occupied += 1;
173 return;
174 }
175 // Linear probing by GROUP_WIDTH (tried triangular — at our 7/8
176 // load factor and group-scan-aware probe, linear wins on cache
177 // locality; triangular's anti-clustering only pays off at higher
178 // load factors than we run).
179 group_start = (group_start + GROUP_WIDTH) & self.mask;
180 }
181 }
182
183 // LOC-WAIVER: per-op probe hot body — deliberate fast/slow loop pair (tombstone-free vs tracking).
184 fn probe_with_key(&self, hash: u64, key: &K) -> ProbeOutcome {
185 if self.cap == 0 {
186 return ProbeOutcome::NotFound { insert_at: 0, via_tombstone: false };
187 }
188 let h2v = h2(hash);
189 let mut group_start = (hash as usize) & self.mask;
190
191 // Fast path: no tombstones in the table ⇒ skip DELETED tracking
192 // entirely. This trims one SIMD `match_byte` (and one branch) from
193 // every group iteration; insert workloads with no deletions hit
194 // this path exclusively.
195 if self.deleted == 0 {
196 loop {
197 // SAFETY: see [insert_known_unique].
198 let g = unsafe { Group::load(self.metadata_ptr.as_ptr().add(group_start)) };
199 for m in g.match_byte(h2v).iter() {
200 let slot = (group_start + m) & self.mask;
201 // SAFETY: matched h2 ⇒ slot is occupied ⇒ initialised.
202 let kv = unsafe { (*self.slots_ptr.as_ptr().add(slot)).assume_init_ref() };
203 if &kv.0 == key {
204 return ProbeOutcome::Found(slot);
205 }
206 }
207 if let Some(m) = g.match_byte(EMPTY).lowest_set() {
208 return ProbeOutcome::NotFound {
209 insert_at: (group_start + m) & self.mask,
210 via_tombstone: false,
211 };
212 }
213 group_start = (group_start + GROUP_WIDTH) & self.mask;
214 }
215 }
216
217 // Slow path: tombstones exist; track the first DELETED so insert
218 // can reclaim it instead of growing the tombstone count.
219 let mut first_deleted: Option<usize> = None;
220 loop {
221 // SAFETY: see [insert_known_unique].
222 let g = unsafe { Group::load(self.metadata_ptr.as_ptr().add(group_start)) };
223 for m in g.match_byte(h2v).iter() {
224 let slot = (group_start + m) & self.mask;
225 // SAFETY: matched h2 ⇒ slot is occupied ⇒ initialised.
226 let kv = unsafe { (*self.slots_ptr.as_ptr().add(slot)).assume_init_ref() };
227 if &kv.0 == key {
228 return ProbeOutcome::Found(slot);
229 }
230 }
231 if first_deleted.is_none()
232 && let Some(m) = g.match_byte(DELETED).lowest_set()
233 {
234 first_deleted = Some((group_start + m) & self.mask);
235 }
236 if let Some(m) = g.match_byte(EMPTY).lowest_set() {
237 let probe_empty = (group_start + m) & self.mask;
238 return ProbeOutcome::NotFound {
239 insert_at: first_deleted.unwrap_or(probe_empty),
240 via_tombstone: first_deleted.is_some(),
241 };
242 }
243 group_start = (group_start + GROUP_WIDTH) & self.mask;
244 }
245 }
246}
247
248impl<K, V> KevyMap<K, V> {
249 /// Borrow the value for `key`, or `None` if absent.
250 /// # Examples
251 ///
252 /// ```
253 /// let mut m = kevy_map::KevyMap::new();
254 /// m.insert(b"k".to_vec(), 1u32);
255 /// // Borrowed lookup: a `&[u8]` finds a `Vec<u8>` key without allocating.
256 /// assert_eq!(m.get(b"k".as_slice()), Some(&1));
257 /// assert_eq!(m.get(b"nope".as_slice()), None);
258 /// ```
259 pub fn get<Q>(&self, key: &Q) -> Option<&V>
260 where
261 K: Borrow<Q>,
262 Q: KevyHash + Eq + ?Sized,
263 {
264 let idx = self.find_by_borrow(key)?;
265 // SAFETY: find_by_borrow only returns indices into full slots.
266 let kv = unsafe { (*self.slots_ptr.as_ptr().add(idx)).assume_init_ref() };
267 Some(&kv.1)
268 }
269
270 /// Mutably borrow the value for `key`, or `None` if absent.
271 /// # Examples
272 ///
273 /// ```
274 /// let mut m = kevy_map::KevyMap::new();
275 /// m.insert(b"k".to_vec(), 1u32);
276 /// if let Some(v) = m.get_mut(b"k".as_slice()) { *v += 41; }
277 /// assert_eq!(m.get(b"k".as_slice()), Some(&42));
278 /// ```
279 pub fn get_mut<Q>(&mut self, key: &Q) -> Option<&mut V>
280 where
281 K: Borrow<Q>,
282 Q: KevyHash + Eq + ?Sized,
283 {
284 let idx = self.find_by_borrow(key)?;
285 // SAFETY: full slot.
286 let kv = unsafe { (*self.slots_ptr.as_ptr().add(idx)).assume_init_mut() };
287 Some(&mut kv.1)
288 }
289
290 /// Whether `key` is present in the map.
291 /// # Examples
292 ///
293 /// ```
294 /// let mut m = kevy_map::KevyMap::new();
295 /// m.insert(b"k".to_vec(), ());
296 /// assert!(m.contains_key(b"k".as_slice()));
297 /// assert!(!m.contains_key(b"j".as_slice()));
298 /// ```
299 pub fn contains_key<Q>(&self, key: &Q) -> bool
300 where
301 K: Borrow<Q>,
302 Q: KevyHash + Eq + ?Sized,
303 {
304 self.find_by_borrow(key).is_some()
305 }
306
307 /// Remove `key`'s entry; returns the previous value if present.
308 /// # Examples
309 ///
310 /// ```
311 /// let mut m = kevy_map::KevyMap::new();
312 /// m.insert(b"k".to_vec(), 7u32);
313 /// assert_eq!(m.remove(b"k".as_slice()), Some(7), "the value comes back");
314 /// assert_eq!(m.remove(b"k".as_slice()), None, "absent — None, not a panic");
315 /// assert!(m.is_empty());
316 /// ```
317 pub fn remove<Q>(&mut self, key: &Q) -> Option<V>
318 where
319 K: Borrow<Q>,
320 Q: KevyHash + Eq + ?Sized,
321 {
322 let idx = self.find_by_borrow(key)?;
323 let mark = self.erase_mark(idx);
324 self.set_meta(idx, mark);
325 self.occupied -= 1;
326 if mark == DELETED {
327 self.deleted += 1;
328 }
329 // SAFETY: slot was full, we just marked it DELETED so it won't be
330 // read again; ptr::read moves the (K, V) out.
331 let (_k, v) = unsafe { ptr::read(self.slots_ptr.as_ptr().add(idx) as *const (K, V)) };
332 Some(v)
333 }
334
335 /// `EMPTY` or `DELETED` for a slot being erased.
336 ///
337 /// A tombstone exists to keep a probe going past a hole. When no
338 /// probe could have walked through this position, the hole stops
339 /// nothing and `EMPTY` is the honest mark — which frees the slot for
340 /// reuse and keeps it out of the load count.
341 ///
342 /// Writing `DELETED` unconditionally cost two things. `deleted` only
343 /// returns to zero at a grow, and both the insert probe and
344 /// `raw_entry` branch on `self.deleted == 0` — so ONE `DEL` put the
345 /// table on its slower probe for the rest of the table's life, an
346 /// extra SIMD compare and branch per group forever. And tombstones
347 /// count toward the load threshold, so they drive doubling: the
348 /// steady state under constant-live churn measured 3,048 tombstones
349 /// against a 3,072 headroom, 24 slots from a doubling that the live
350 /// set never asked for.
351 fn erase_mark(&self, idx: usize) -> u8 {
352 // hashbrown's rule, and the reason it is not "does this slot's
353 // group hold an EMPTY": probes here start at `hash & mask` and
354 // are NOT group-aligned, so the sequence that placed a later key
355 // may have entered from any of the `GROUP_WIDTH - 1` positions
356 // before this one. What decides it is whether a run of at least
357 // `GROUP_WIDTH` non-empty slots spans this position — if one
358 // does, some probe could have walked through, and the hole has
359 // to stay a tombstone.
360 //
361 // A simpler condition was tried and it lost a key:
362 // `clone_after_heavy_deletion_keeps_probes_correct` found it
363 // immediately, which is what that test is for.
364 let before = idx.wrapping_sub(GROUP_WIDTH) & self.mask;
365 // SAFETY: both indices are < cap and the metadata array is
366 // `cap + GROUP_WIDTH` bytes, so either group load is in bounds.
367 let (gb, ga) = unsafe {
368 (
369 Group::load(self.metadata_ptr.as_ptr().add(before)),
370 Group::load(self.metadata_ptr.as_ptr().add(idx)),
371 )
372 };
373 let empty_before = gb.match_byte(EMPTY).slot_mask().leading_zeros() as usize;
374 let empty_after = ga.match_byte(EMPTY).slot_mask().trailing_zeros() as usize;
375 if empty_before + empty_after >= GROUP_WIDTH { DELETED } else { EMPTY }
376 }
377
378 pub(crate) fn find_by_borrow<Q>(&self, key: &Q) -> Option<usize>
379 where
380 K: Borrow<Q>,
381 Q: KevyHash + Eq + ?Sized,
382 {
383 if self.cap == 0 {
384 return None;
385 }
386 let hash = key.kevy_hash();
387 let h2v = h2(hash);
388 let mut group_start = (hash as usize) & self.mask;
389 loop {
390 // SAFETY: see [insert_known_unique]; group_start ∈ [0, cap),
391 // metadata length ≥ cap + GROUP_WIDTH.
392 let g = unsafe { Group::load(self.metadata_ptr.as_ptr().add(group_start)) };
393 for m in g.match_byte(h2v).iter() {
394 let slot = (group_start + m) & self.mask;
395 // SAFETY: matched h2 ⇒ slot occupied ⇒ initialised.
396 let kv = unsafe { (*self.slots_ptr.as_ptr().add(slot)).assume_init_ref() };
397 if kv.0.borrow() == key {
398 return Some(slot);
399 }
400 }
401 // EMPTY in this group ⇒ key cannot be later in the probe.
402 if !g.match_byte(EMPTY).is_empty() {
403 return None;
404 }
405 group_start = (group_start + GROUP_WIDTH) & self.mask;
406 }
407 }
408
409 /// Full probe: returns `Found(idx)` if `key` is present, else
410 /// `NotFound { insert_at, via_tombstone }` describing the slot a future
411 /// insert would take. Mirrors [`probe_with_key`](Self::probe_with_key)
412 /// but accepts a `Borrow<Q>` key.
413 ///
414 /// Used by the [`raw_entry_mut`](Self::raw_entry_mut) API to fuse a read
415 /// and a possible insert into a single probe.
416 pub(crate) fn probe_by_borrow<Q>(&self, key: &Q) -> ProbeOutcome
417 where
418 K: Borrow<Q>,
419 Q: KevyHash + Eq + ?Sized,
420 {
421 if self.cap == 0 {
422 return ProbeOutcome::NotFound { insert_at: 0, via_tombstone: false };
423 }
424 let hash = key.kevy_hash();
425 let h2v = h2(hash);
426 let group_start = (hash as usize) & self.mask;
427 if self.deleted == 0 {
428 self.probe_by_borrow_fast(key, h2v, group_start)
429 } else {
430 self.probe_by_borrow_slow(key, h2v, group_start)
431 }
432 }
433
434 /// Fast path for `probe_by_borrow`: no tombstones in the table, so we
435 /// can stop tracking DELETED slots entirely.
436 fn probe_by_borrow_fast<Q>(&self, key: &Q, h2v: u8, mut group_start: usize) -> ProbeOutcome
437 where
438 K: Borrow<Q>,
439 Q: KevyHash + Eq + ?Sized,
440 {
441 loop {
442 // SAFETY: see [insert_known_unique].
443 let g = unsafe { Group::load(self.metadata_ptr.as_ptr().add(group_start)) };
444 for m in g.match_byte(h2v).iter() {
445 let slot = (group_start + m) & self.mask;
446 // SAFETY: matched h2 ⇒ slot occupied ⇒ initialised.
447 let kv = unsafe { (*self.slots_ptr.as_ptr().add(slot)).assume_init_ref() };
448 if kv.0.borrow() == key {
449 return ProbeOutcome::Found(slot);
450 }
451 }
452 if let Some(m) = g.match_byte(EMPTY).lowest_set() {
453 return ProbeOutcome::NotFound {
454 insert_at: (group_start + m) & self.mask,
455 via_tombstone: false,
456 };
457 }
458 group_start = (group_start + GROUP_WIDTH) & self.mask;
459 }
460 }
461
462 /// Slow path for `probe_by_borrow`: tombstones present; remember the
463 /// first DELETED so a later insert can reclaim it.
464 fn probe_by_borrow_slow<Q>(&self, key: &Q, h2v: u8, mut group_start: usize) -> ProbeOutcome
465 where
466 K: Borrow<Q>,
467 Q: KevyHash + Eq + ?Sized,
468 {
469 let mut first_deleted: Option<usize> = None;
470 loop {
471 // SAFETY: see [insert_known_unique].
472 let g = unsafe { Group::load(self.metadata_ptr.as_ptr().add(group_start)) };
473 for m in g.match_byte(h2v).iter() {
474 let slot = (group_start + m) & self.mask;
475 // SAFETY: matched h2 ⇒ slot occupied ⇒ initialised.
476 let kv = unsafe { (*self.slots_ptr.as_ptr().add(slot)).assume_init_ref() };
477 if kv.0.borrow() == key {
478 return ProbeOutcome::Found(slot);
479 }
480 }
481 if first_deleted.is_none()
482 && let Some(m) = g.match_byte(DELETED).lowest_set()
483 {
484 first_deleted = Some((group_start + m) & self.mask);
485 }
486 if let Some(m) = g.match_byte(EMPTY).lowest_set() {
487 let probe_empty = (group_start + m) & self.mask;
488 return ProbeOutcome::NotFound {
489 insert_at: first_deleted.unwrap_or(probe_empty),
490 via_tombstone: first_deleted.is_some(),
491 };
492 }
493 group_start = (group_start + GROUP_WIDTH) & self.mask;
494 }
495 }
496}