Skip to main content

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}