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 lives in `grow.rs`.
45
46use core::borrow::Borrow;
47use core::ptr;
48
49use kevy_hash::KevyHash;
50
51use crate::group::Group;
52use crate::map::{DELETED, EMPTY, GROUP_WIDTH, KevyMap, ProbeOutcome, h2};
53
54impl<K: KevyHash + Eq, V> KevyMap<K, V> {
55 /// Insert `(key, value)`. Returns the old value if `key` was already
56 /// present. Following `std::HashMap` semantics, the existing K is kept on
57 /// overwrite — only V is replaced.
58 /// # Examples
59 ///
60 /// ```
61 /// let mut m = kevy_map::KevyMap::new();
62 /// assert_eq!(m.insert(b"k".to_vec(), 1u32), None, "no previous value");
63 /// assert_eq!(m.insert(b"k".to_vec(), 2), Some(1), "the OLD value comes back");
64 /// assert_eq!(m.get(b"k".as_slice()), Some(&2));
65 /// ```
66 pub fn insert(&mut self, key: K, value: V) -> Option<V> {
67 self.maybe_grow();
68 self.insert_with_room(key, value)
69 }
70
71 /// [`Self::insert`] after the growth check, which the caller has made.
72 #[inline]
73 pub(crate) fn insert_with_room(&mut self, key: K, value: V) -> Option<V> {
74 let hash = key.kevy_hash();
75 match self.probe_with_key(hash, &key) {
76 ProbeOutcome::Found(idx) => {
77 // SAFETY: `idx` came from a probe that found a full metadata byte, so the
78 // slot at `idx` holds an initialised `(K, V)`; only its V is replaced.
79 let v_ptr = unsafe {
80 let kv: *mut (K, V) = self.slots_ptr.as_ptr().add(idx).cast::<(K, V)>();
81 ptr::addr_of_mut!((*kv).1)
82 };
83 // SAFETY: `v_ptr` points at that slot's initialised `V`, so the old value
84 // is a valid `V` to move out and the new one is written in its place.
85 let old_v = unsafe { ptr::replace(v_ptr, value) };
86 drop(key);
87 Some(old_v)
88 }
89 ProbeOutcome::NotFound { insert_at, via_tombstone } => {
90 self.set_meta(insert_at, h2(hash));
91 // SAFETY: insert_at < cap ⇒ slot pointer in-bounds; we write
92 // (K, V) into a previously uninitialised slot.
93 unsafe {
94 (*self.slots_ptr.as_ptr().add(insert_at)).write((key, value));
95 }
96 self.occupied += 1;
97 if via_tombstone {
98 self.deleted -= 1;
99 }
100 None
101 }
102 }
103 }
104
105 pub(crate) fn maybe_grow(&mut self) {
106 if self.grows_on_insert() {
107 self.grow();
108 }
109 }
110
111 // LOC-WAIVER: per-op probe hot body — deliberate fast/slow loop pair (tombstone-free vs tracking).
112 fn probe_with_key(&self, hash: u64, key: &K) -> ProbeOutcome {
113 if self.cap == 0 {
114 return ProbeOutcome::NotFound { insert_at: 0, via_tombstone: false };
115 }
116 let h2v = h2(hash);
117 let mut group_start = (hash as usize) & self.mask;
118
119 // Fast path: no tombstones in the table ⇒ skip DELETED tracking
120 // entirely. This trims one SIMD `match_byte` (and one branch) from
121 // every group iteration; insert workloads with no deletions hit
122 // this path exclusively.
123 if self.deleted == 0 {
124 loop {
125 // SAFETY: see [insert_known_unique].
126 let g = unsafe { Group::load(self.metadata_ptr.as_ptr().add(group_start)) };
127 for m in g.match_byte(h2v).iter() {
128 let slot = (group_start + m) & self.mask;
129 // SAFETY: matched h2 ⇒ slot is occupied ⇒ initialised.
130 let kv = unsafe { (*self.slots_ptr.as_ptr().add(slot)).assume_init_ref() };
131 if &kv.0 == key {
132 return ProbeOutcome::Found(slot);
133 }
134 }
135 if let Some(m) = g.match_byte(EMPTY).lowest_set() {
136 return ProbeOutcome::NotFound {
137 insert_at: (group_start + m) & self.mask,
138 via_tombstone: false,
139 };
140 }
141 group_start = (group_start + GROUP_WIDTH) & self.mask;
142 }
143 }
144
145 // Slow path: tombstones exist; track the first DELETED so insert
146 // can reclaim it instead of growing the tombstone count.
147 let mut first_deleted: Option<usize> = None;
148 loop {
149 // SAFETY: see [insert_known_unique].
150 let g = unsafe { Group::load(self.metadata_ptr.as_ptr().add(group_start)) };
151 for m in g.match_byte(h2v).iter() {
152 let slot = (group_start + m) & self.mask;
153 // SAFETY: matched h2 ⇒ slot is occupied ⇒ initialised.
154 let kv = unsafe { (*self.slots_ptr.as_ptr().add(slot)).assume_init_ref() };
155 if &kv.0 == key {
156 return ProbeOutcome::Found(slot);
157 }
158 }
159 if first_deleted.is_none()
160 && let Some(m) = g.match_byte(DELETED).lowest_set()
161 {
162 first_deleted = Some((group_start + m) & self.mask);
163 }
164 if let Some(m) = g.match_byte(EMPTY).lowest_set() {
165 let probe_empty = (group_start + m) & self.mask;
166 return ProbeOutcome::NotFound {
167 insert_at: first_deleted.unwrap_or(probe_empty),
168 via_tombstone: first_deleted.is_some(),
169 };
170 }
171 group_start = (group_start + GROUP_WIDTH) & self.mask;
172 }
173 }
174}
175
176impl<K, V> KevyMap<K, V> {
177 /// Borrow the value for `key`, or `None` if absent.
178 /// # Examples
179 ///
180 /// ```
181 /// let mut m = kevy_map::KevyMap::new();
182 /// m.insert(b"k".to_vec(), 1u32);
183 /// // Borrowed lookup: a `&[u8]` finds a `Vec<u8>` key without allocating.
184 /// assert_eq!(m.get(b"k".as_slice()), Some(&1));
185 /// assert_eq!(m.get(b"nope".as_slice()), None);
186 /// ```
187 pub fn get<Q>(&self, key: &Q) -> Option<&V>
188 where
189 K: Borrow<Q>,
190 Q: KevyHash + Eq + ?Sized,
191 {
192 let idx = self.find_by_borrow(key)?;
193 // SAFETY: find_by_borrow only returns indices into full slots.
194 let kv = unsafe { (*self.slots_ptr.as_ptr().add(idx)).assume_init_ref() };
195 Some(&kv.1)
196 }
197
198 /// Mutably borrow the value for `key`, or `None` if absent.
199 /// # Examples
200 ///
201 /// ```
202 /// let mut m = kevy_map::KevyMap::new();
203 /// m.insert(b"k".to_vec(), 1u32);
204 /// if let Some(v) = m.get_mut(b"k".as_slice()) { *v += 41; }
205 /// assert_eq!(m.get(b"k".as_slice()), Some(&42));
206 /// ```
207 pub fn get_mut<Q>(&mut self, key: &Q) -> Option<&mut V>
208 where
209 K: Borrow<Q>,
210 Q: KevyHash + Eq + ?Sized,
211 {
212 let idx = self.find_by_borrow(key)?;
213 // SAFETY: full slot.
214 let kv = unsafe { (*self.slots_ptr.as_ptr().add(idx)).assume_init_mut() };
215 Some(&mut kv.1)
216 }
217
218 /// Whether `key` is present in the map.
219 /// # Examples
220 ///
221 /// ```
222 /// let mut m = kevy_map::KevyMap::new();
223 /// m.insert(b"k".to_vec(), ());
224 /// assert!(m.contains_key(b"k".as_slice()));
225 /// assert!(!m.contains_key(b"j".as_slice()));
226 /// ```
227 pub fn contains_key<Q>(&self, key: &Q) -> bool
228 where
229 K: Borrow<Q>,
230 Q: KevyHash + Eq + ?Sized,
231 {
232 self.find_by_borrow(key).is_some()
233 }
234
235 /// Remove `key`'s entry; returns the previous value if present.
236 /// # Examples
237 ///
238 /// ```
239 /// let mut m = kevy_map::KevyMap::new();
240 /// m.insert(b"k".to_vec(), 7u32);
241 /// assert_eq!(m.remove(b"k".as_slice()), Some(7), "the value comes back");
242 /// assert_eq!(m.remove(b"k".as_slice()), None, "absent — None, not a panic");
243 /// assert!(m.is_empty());
244 /// ```
245 pub fn remove<Q>(&mut self, key: &Q) -> Option<V>
246 where
247 K: Borrow<Q>,
248 Q: KevyHash + Eq + ?Sized,
249 {
250 let idx = self.find_by_borrow(key)?;
251 let mark = self.erase_mark(idx);
252 self.set_meta(idx, mark);
253 self.occupied -= 1;
254 if mark == DELETED {
255 self.deleted += 1;
256 }
257 // SAFETY: slot was full, we just marked it DELETED so it won't be
258 // read again; ptr::read moves the (K, V) out.
259 let (_k, v) = unsafe { ptr::read(self.slots_ptr.as_ptr().add(idx) as *const (K, V)) };
260 Some(v)
261 }
262
263 /// `EMPTY` or `DELETED` for a slot being erased.
264 ///
265 /// A tombstone exists to keep a probe going past a hole. When no
266 /// probe could have walked through this position, the hole stops
267 /// nothing and `EMPTY` is the honest mark — which frees the slot for
268 /// reuse and keeps it out of the load count.
269 ///
270 /// Writing `DELETED` unconditionally cost two things. `deleted` only
271 /// returns to zero at a grow, and both the insert probe and
272 /// `raw_entry` branch on `self.deleted == 0` — so ONE `DEL` put the
273 /// table on its slower probe for the rest of the table's life, an
274 /// extra SIMD compare and branch per group forever. And tombstones
275 /// count toward the load threshold, so they drive doubling: the
276 /// steady state under constant-live churn measured 3,048 tombstones
277 /// against a 3,072 headroom, 24 slots from a doubling that the live
278 /// set never asked for.
279 fn erase_mark(&self, idx: usize) -> u8 {
280 // hashbrown's rule, and the reason it is not "does this slot's
281 // group hold an EMPTY": probes here start at `hash & mask` and
282 // are NOT group-aligned, so the sequence that placed a later key
283 // may have entered from any of the `GROUP_WIDTH - 1` positions
284 // before this one. What decides it is whether a run of at least
285 // `GROUP_WIDTH` non-empty slots spans this position — if one
286 // does, some probe could have walked through, and the hole has
287 // to stay a tombstone.
288 //
289 // A simpler condition was tried and it lost a key:
290 // `clone_after_heavy_deletion_keeps_probes_correct` found it
291 // immediately, which is what that test is for.
292 let before = idx.wrapping_sub(GROUP_WIDTH) & self.mask;
293 // SAFETY: both indices are < cap and the metadata array is
294 // `cap + GROUP_WIDTH` bytes, so either group load is in bounds.
295 let (gb, ga) = unsafe {
296 (
297 Group::load(self.metadata_ptr.as_ptr().add(before)),
298 Group::load(self.metadata_ptr.as_ptr().add(idx)),
299 )
300 };
301 let empty_before = gb.match_byte(EMPTY).slot_mask().leading_zeros() as usize;
302 let empty_after = ga.match_byte(EMPTY).slot_mask().trailing_zeros() as usize;
303 if empty_before + empty_after >= GROUP_WIDTH { DELETED } else { EMPTY }
304 }
305
306 pub(crate) fn find_by_borrow<Q>(&self, key: &Q) -> Option<usize>
307 where
308 K: Borrow<Q>,
309 Q: KevyHash + Eq + ?Sized,
310 {
311 if self.cap == 0 {
312 return None;
313 }
314 let hash = key.kevy_hash();
315 let h2v = h2(hash);
316 let mut group_start = (hash as usize) & self.mask;
317 loop {
318 // SAFETY: see [insert_known_unique]; group_start ∈ [0, cap),
319 // metadata length ≥ cap + GROUP_WIDTH.
320 let g = unsafe { Group::load(self.metadata_ptr.as_ptr().add(group_start)) };
321 for m in g.match_byte(h2v).iter() {
322 let slot = (group_start + m) & self.mask;
323 // SAFETY: matched h2 ⇒ slot occupied ⇒ initialised.
324 let kv = unsafe { (*self.slots_ptr.as_ptr().add(slot)).assume_init_ref() };
325 if kv.0.borrow() == key {
326 return Some(slot);
327 }
328 }
329 // EMPTY in this group ⇒ key cannot be later in the probe.
330 if !g.match_byte(EMPTY).is_empty() {
331 return None;
332 }
333 group_start = (group_start + GROUP_WIDTH) & self.mask;
334 }
335 }
336
337 /// Full probe: returns `Found(idx)` if `key` is present, else
338 /// `NotFound { insert_at, via_tombstone }` describing the slot a future
339 /// insert would take. Mirrors [`probe_with_key`](Self::probe_with_key)
340 /// but accepts a `Borrow<Q>` key.
341 ///
342 /// Used by the [`raw_entry_mut`](Self::raw_entry_mut) API to fuse a read
343 /// and a possible insert into a single probe.
344 pub(crate) fn probe_by_borrow<Q>(&self, key: &Q) -> ProbeOutcome
345 where
346 K: Borrow<Q>,
347 Q: KevyHash + Eq + ?Sized,
348 {
349 if self.cap == 0 {
350 return ProbeOutcome::NotFound { insert_at: 0, via_tombstone: false };
351 }
352 let hash = key.kevy_hash();
353 let h2v = h2(hash);
354 let group_start = (hash as usize) & self.mask;
355 if self.deleted == 0 {
356 self.probe_by_borrow_fast(key, h2v, group_start)
357 } else {
358 self.probe_by_borrow_slow(key, h2v, group_start)
359 }
360 }
361
362 /// Fast path for `probe_by_borrow`: no tombstones in the table, so we
363 /// can stop tracking DELETED slots entirely.
364 fn probe_by_borrow_fast<Q>(&self, key: &Q, h2v: u8, mut group_start: usize) -> ProbeOutcome
365 where
366 K: Borrow<Q>,
367 Q: KevyHash + Eq + ?Sized,
368 {
369 loop {
370 // SAFETY: see [insert_known_unique].
371 let g = unsafe { Group::load(self.metadata_ptr.as_ptr().add(group_start)) };
372 for m in g.match_byte(h2v).iter() {
373 let slot = (group_start + m) & self.mask;
374 // SAFETY: matched h2 ⇒ slot occupied ⇒ initialised.
375 let kv = unsafe { (*self.slots_ptr.as_ptr().add(slot)).assume_init_ref() };
376 if kv.0.borrow() == key {
377 return ProbeOutcome::Found(slot);
378 }
379 }
380 if let Some(m) = g.match_byte(EMPTY).lowest_set() {
381 return ProbeOutcome::NotFound {
382 insert_at: (group_start + m) & self.mask,
383 via_tombstone: false,
384 };
385 }
386 group_start = (group_start + GROUP_WIDTH) & self.mask;
387 }
388 }
389
390 /// Slow path for `probe_by_borrow`: tombstones present; remember the
391 /// first DELETED so a later insert can reclaim it.
392 fn probe_by_borrow_slow<Q>(&self, key: &Q, h2v: u8, mut group_start: usize) -> ProbeOutcome
393 where
394 K: Borrow<Q>,
395 Q: KevyHash + Eq + ?Sized,
396 {
397 let mut first_deleted: Option<usize> = None;
398 loop {
399 // SAFETY: see [insert_known_unique].
400 let g = unsafe { Group::load(self.metadata_ptr.as_ptr().add(group_start)) };
401 for m in g.match_byte(h2v).iter() {
402 let slot = (group_start + m) & self.mask;
403 // SAFETY: matched h2 ⇒ slot occupied ⇒ initialised.
404 let kv = unsafe { (*self.slots_ptr.as_ptr().add(slot)).assume_init_ref() };
405 if kv.0.borrow() == key {
406 return ProbeOutcome::Found(slot);
407 }
408 }
409 if first_deleted.is_none()
410 && let Some(m) = g.match_byte(DELETED).lowest_set()
411 {
412 first_deleted = Some((group_start + m) & self.mask);
413 }
414 if let Some(m) = g.match_byte(EMPTY).lowest_set() {
415 let probe_empty = (group_start + m) & self.mask;
416 return ProbeOutcome::NotFound {
417 insert_at: first_deleted.unwrap_or(probe_empty),
418 via_tombstone: first_deleted.is_some(),
419 };
420 }
421 group_start = (group_start + GROUP_WIDTH) & self.mask;
422 }
423 }
424}