yo_index/index.rs
1//! The index: a directory of segments, dashtable style growth, no stop the
2//! world rehash.
3//!
4//! `05` section 2.2. The index is an array of segment pointers. Each segment is
5//! a run of buckets with a local depth. When a bucket and its overflow chain
6//! are both full, the segment splits: a new segment is allocated, entries are
7//! redistributed by the next hash bit, and the directory entries that named the
8//! old segment are repointed. If the local depth would pass the global depth,
9//! the directory doubles first, which is one allocation and a memcpy of
10//! pointers.
11//!
12//! A split touches one segment. Nothing else in the shard stops.
13//!
14//! # Which bits do what
15//!
16//! ```text
17//! 63 56 55 N 0
18//! +--------------+-----------------------+--------------------------+
19//! | tag | directory index | bucket within segment |
20//! +--------------+-----------------------+--------------------------+
21//! ```
22//!
23//! The tag is the top eight bits, the directory takes the next `global_depth`
24//! bits, and the bucket index comes off the bottom. They are disjoint on
25//! purpose: if the directory used the top bits, every key in a segment would
26//! share a tag prefix and the prefilter would stop filtering.
27
28use crate::bucket::{Bucket, SLOTS};
29use crate::scan::{Cursor, PREFIX_BITS};
30use yo_common::{Addr, tag_of};
31
32/// Buckets in one index segment. Sixty four buckets is 4 KiB, which is one page
33/// and 448 entries.
34pub const SEGMENT_BUCKETS: usize = 64;
35
36/// Overflow buckets a chain may hold before the segment splits instead.
37pub const MAX_CHAIN: usize = 2;
38
39/// Bits available to the directory, below the tag.
40pub(crate) const DIR_BITS: u32 = 56;
41
42/// The deepest the directory may go. Past this the hash has no bits left to
43/// discriminate on and the only honest answer is a longer chain.
44const MAX_DEPTH: u8 = 48;
45
46struct Segment {
47 buckets: Vec<Bucket>,
48 overflow: Vec<Bucket>,
49 local_depth: u8,
50}
51
52impl Segment {
53 fn new(local_depth: u8) -> Segment {
54 Segment {
55 buckets: vec![Bucket::EMPTY; SEGMENT_BUCKETS],
56 overflow: Vec::new(),
57 local_depth,
58 }
59 }
60}
61
62/// What the index needs to know about the records its addresses point at.
63///
64/// The index stores a tag and an address, not a key and not a hash. A split has
65/// to recompute which side of the next bit each entry falls on, and a probe has
66/// to confirm a tag match, so both need to reach the key bytes. Keeping that
67/// behind a trait is what lets the index stay independent of the record format,
68/// which changes in M1 and again when documents arrive.
69pub trait Keys {
70 /// The full hash of the key stored at `addr`.
71 fn hash_at(&self, addr: Addr) -> u64;
72
73 /// Whether the key stored at `addr` is `key`.
74 fn eq_at(&self, addr: Addr, key: &[u8]) -> bool;
75}
76
77/// The shard's index.
78#[derive(Debug)]
79pub struct Index {
80 dir: Vec<u32>,
81 segs: Vec<Segment>,
82 global_depth: u8,
83 len: usize,
84 splits: u64,
85 doublings: u64,
86}
87
88impl core::fmt::Debug for Segment {
89 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
90 f.debug_struct("Segment")
91 .field("local_depth", &self.local_depth)
92 .field("overflow", &self.overflow.len())
93 .finish()
94 }
95}
96
97impl Index {
98 /// A new index with one segment.
99 pub fn new() -> Index {
100 Index {
101 dir: vec![0],
102 segs: vec![Segment::new(0)],
103 global_depth: 0,
104 len: 0,
105 splits: 0,
106 doublings: 0,
107 }
108 }
109
110 /// How many entries the index holds.
111 #[inline]
112 pub fn len(&self) -> usize {
113 self.len
114 }
115
116 /// Whether the index holds nothing.
117 #[inline]
118 pub fn is_empty(&self) -> bool {
119 self.len == 0
120 }
121
122 /// How many segments exist.
123 pub fn segment_count(&self) -> usize {
124 self.segs.len()
125 }
126
127 /// The current global depth.
128 pub fn global_depth(&self) -> u8 {
129 self.global_depth
130 }
131
132 /// How many segment splits have happened over the life of this index.
133 pub fn splits(&self) -> u64 {
134 self.splits
135 }
136
137 /// How many times the directory has doubled.
138 pub fn doublings(&self) -> u64 {
139 self.doublings
140 }
141
142 /// Bytes of index structure, for `INFO memory`.
143 pub fn memory_bytes(&self) -> usize {
144 self.dir.len() * size_of::<u32>()
145 + self
146 .segs
147 .iter()
148 .map(|s| (s.buckets.len() + s.overflow.len()) * size_of::<Bucket>())
149 .sum::<usize>()
150 }
151
152 #[inline(always)]
153 fn dir_index(&self, hash: u64) -> usize {
154 let d = self.global_depth as u32;
155 if d == 0 {
156 return 0;
157 }
158 ((hash >> (DIR_BITS - d)) & ((1u64 << d) - 1)) as usize
159 }
160
161 #[inline(always)]
162 fn bucket_index(hash: u64) -> usize {
163 (hash as usize) & (SEGMENT_BUCKETS - 1)
164 }
165
166 /// Ask the cache for the bucket `hash` is going to land in.
167 ///
168 /// The first of the two walks in `04` section 3. It costs one instruction,
169 /// it reads nothing, and it is the difference between a batch of 64 lookups
170 /// paying 64 serial cache misses and paying them overlapped. Only the first
171 /// bucket is asked for: the directory entry and the segment header are on
172 /// the way to it anyway, and an overflow bucket is a 1 in a few hundred
173 /// event that is not worth a second hint.
174 ///
175 /// Calling this and then never doing the lookup is allowed and wastes a
176 /// little bandwidth. Calling it with a hash from a different key is also
177 /// allowed and does the same, which is the whole reason a hint is a hint.
178 #[inline(always)]
179 pub fn prefetch(&self, hash: u64) {
180 let seg = &self.segs[self.dir[self.dir_index(hash)] as usize];
181 yo_common::prefetch(&seg.buckets[Self::bucket_index(hash)]);
182 }
183
184 /// Find the address stored under `key`.
185 ///
186 /// The hot path, and the one M0's four nanosecond gate measures. One load
187 /// of the bucket, one SWAR compare of seven tags, and a key comparison per
188 /// surviving match, which is one comparison in the overwhelming majority of
189 /// probes because a tag collision is a 1 in 256 event.
190 #[inline]
191 pub fn get<K: Keys>(&self, hash: u64, key: &[u8], keys: &K) -> Option<Addr> {
192 let tag = tag_of(hash);
193 let seg = &self.segs[self.dir[self.dir_index(hash)] as usize];
194 let mut b = &seg.buckets[Self::bucket_index(hash)];
195 loop {
196 for i in b.match_tag(tag) {
197 let addr = b.addr(i);
198 if keys.eq_at(addr, key) {
199 return Some(addr);
200 }
201 }
202 {
203 let next = b.link()?;
204 b = &seg.overflow[(next - 1) as usize]
205 }
206 }
207 }
208
209 /// Whether `key` is present.
210 #[inline]
211 pub fn contains<K: Keys>(&self, hash: u64, key: &[u8], keys: &K) -> bool {
212 self.get(hash, key, keys).is_some()
213 }
214
215 /// Insert or replace the address stored under `key`.
216 ///
217 /// Returns the address that was there before, if any. The caller owns what
218 /// that address points at, so freeing it is the caller's job. The index
219 /// does not know how big a record is and will not guess.
220 pub fn insert<K: Keys>(&mut self, hash: u64, key: &[u8], addr: Addr, keys: &K) -> Option<Addr> {
221 debug_assert!(addr.is_some(), "the index cannot store the absent address");
222 let tag = tag_of(hash);
223
224 loop {
225 let seg_idx = self.dir[self.dir_index(hash)] as usize;
226 let bucket_idx = Self::bucket_index(hash);
227
228 // Replace in place if the key is already here, and remember the
229 // first free slot on the way through so that a miss does not walk
230 // the chain twice.
231 let mut free: Option<(usize, usize)> = None;
232 let mut chain_len = 0usize;
233 let mut cursor: Option<usize> = None;
234
235 loop {
236 let seg = &self.segs[seg_idx];
237 let b = match cursor {
238 None => &seg.buckets[bucket_idx],
239 Some(o) => &seg.overflow[o],
240 };
241 for i in b.match_tag(tag) {
242 if keys.eq_at(b.addr(i), key) {
243 let old = b.addr(i);
244 let seg = &mut self.segs[seg_idx];
245 let b = match cursor {
246 None => &mut seg.buckets[bucket_idx],
247 Some(o) => &mut seg.overflow[o],
248 };
249 b.set_addr(i, addr);
250 return Some(old);
251 }
252 }
253 if free.is_none()
254 && let Some(i) = b.match_empty().first()
255 {
256 free = Some((cursor.unwrap_or(usize::MAX), i));
257 }
258 match b.link() {
259 Some(next) => {
260 cursor = Some((next - 1) as usize);
261 chain_len += 1;
262 }
263 None => break,
264 }
265 }
266
267 if let Some((where_, slot)) = free {
268 let seg = &mut self.segs[seg_idx];
269 let b = if where_ == usize::MAX {
270 &mut seg.buckets[bucket_idx]
271 } else {
272 &mut seg.overflow[where_]
273 };
274 b.set(slot, tag, addr);
275 self.len += 1;
276 return None;
277 }
278
279 // Nothing free anywhere in the chain.
280 if chain_len < MAX_CHAIN || self.segs[seg_idx].local_depth >= MAX_DEPTH {
281 self.extend_chain(seg_idx, bucket_idx, cursor, tag, addr);
282 self.len += 1;
283 return None;
284 }
285
286 self.split(seg_idx, keys);
287 // The directory moved under us, so start over rather than trying to
288 // reason about where this key landed.
289 }
290 }
291
292 fn extend_chain(
293 &mut self,
294 seg_idx: usize,
295 bucket_idx: usize,
296 tail: Option<usize>,
297 tag: u8,
298 addr: Addr,
299 ) {
300 let seg = &mut self.segs[seg_idx];
301 let mut fresh = Bucket::EMPTY;
302 fresh.set(0, tag, addr);
303 yo_alloc::allow(|| seg.overflow.push(fresh));
304 let new_idx = seg.overflow.len() - 1;
305 let link = (new_idx + 1) as u64;
306 match tail {
307 None => seg.buckets[bucket_idx].set_link(link),
308 Some(o) => seg.overflow[o].set_link(link),
309 }
310 }
311
312 /// Remove `key`.
313 ///
314 /// Returns the address that was stored, if any. Tombstone free: the tag
315 /// goes back to zero. A probe stops at the first empty tag in the chain
316 /// rather than in the bucket, so nothing has to be pulled back.
317 pub fn remove<K: Keys>(&mut self, hash: u64, key: &[u8], keys: &K) -> Option<Addr> {
318 let tag = tag_of(hash);
319 let seg_idx = self.dir[self.dir_index(hash)] as usize;
320 let bucket_idx = Self::bucket_index(hash);
321 let mut cursor: Option<usize> = None;
322
323 loop {
324 let seg = &self.segs[seg_idx];
325 let b = match cursor {
326 None => &seg.buckets[bucket_idx],
327 Some(o) => &seg.overflow[o],
328 };
329 let mut hit = None;
330 for i in b.match_tag(tag) {
331 if keys.eq_at(b.addr(i), key) {
332 hit = Some((i, b.addr(i)));
333 break;
334 }
335 }
336 if let Some((i, addr)) = hit {
337 let seg = &mut self.segs[seg_idx];
338 let b = match cursor {
339 None => &mut seg.buckets[bucket_idx],
340 Some(o) => &mut seg.overflow[o],
341 };
342 b.clear(i);
343 self.len -= 1;
344 return Some(addr);
345 }
346 let next = {
347 let seg = &self.segs[seg_idx];
348 let b = match cursor {
349 None => &seg.buckets[bucket_idx],
350 Some(o) => &seg.overflow[o],
351 };
352 b.link()
353 };
354 {
355 let n = next?;
356 cursor = Some((n - 1) as usize)
357 }
358 }
359 }
360
361 /// Split one segment by the next hash bit.
362 fn split<K: Keys>(&mut self, seg_idx: usize, keys: &K) {
363 let ld = self.segs[seg_idx].local_depth;
364 if ld == self.global_depth {
365 self.double_directory();
366 }
367 let gd = self.global_depth;
368 debug_assert!(ld < gd);
369
370 self.segs[seg_idx].local_depth = ld + 1;
371 yo_alloc::allow(|| self.segs.push(Segment::new(ld + 1)));
372 let new_idx = self.segs.len() - 1;
373 self.splits += 1;
374
375 // Repoint the half of the directory entries whose bit `ld`, counted
376 // from the top of a `gd` bit index, is one.
377 let shift = (gd - 1 - ld) as u32;
378 for i in 0..self.dir.len() {
379 if self.dir[i] as usize == seg_idx && ((i >> shift) & 1) == 1 {
380 self.dir[i] = new_idx as u32;
381 }
382 }
383
384 // Move every entry that now belongs to the new segment. The hash is
385 // recomputed from the key rather than stored, which is the cost of
386 // spending all 56 non tag bits on addressing instead of on a cached
387 // hash. It is paid once per entry per split, and a split is a
388 // `log2(n / segment capacity)` event.
389 let mut moving: Vec<(u64, Addr)> = Vec::new();
390 yo_alloc::allow(|| {
391 let seg = &mut self.segs[seg_idx];
392 let mut visit = |b: &mut Bucket| {
393 for i in 0..SLOTS {
394 if b.tag(i) == crate::bucket::EMPTY {
395 continue;
396 }
397 let addr = b.addr(i);
398 let h = keys.hash_at(addr);
399 if ((h >> (DIR_BITS - gd as u32)) & ((1u64 << gd) - 1)) >> shift & 1 == 1 {
400 moving.push((h, addr));
401 b.clear(i);
402 }
403 }
404 };
405 for b in seg.buckets.iter_mut() {
406 visit(b);
407 }
408 for b in seg.overflow.iter_mut() {
409 visit(b);
410 }
411 });
412
413 for (h, addr) in moving {
414 self.place_raw(new_idx, h, addr);
415 }
416 }
417
418 /// Put an entry into a known segment without any lookup.
419 ///
420 /// Only correct during a split, where the entry is known to be absent from
421 /// the destination because it was just removed from the source.
422 fn place_raw(&mut self, seg_idx: usize, hash: u64, addr: Addr) {
423 let tag = tag_of(hash);
424 let bucket_idx = Self::bucket_index(hash);
425 let mut cursor: Option<usize> = None;
426 loop {
427 let seg = &mut self.segs[seg_idx];
428 let b = match cursor {
429 None => &mut seg.buckets[bucket_idx],
430 Some(o) => &mut seg.overflow[o],
431 };
432 if let Some(i) = b.match_empty().first() {
433 b.set(i, tag, addr);
434 return;
435 }
436 match b.link() {
437 Some(n) => cursor = Some((n - 1) as usize),
438 None => {
439 self.extend_chain(seg_idx, bucket_idx, cursor, tag, addr);
440 return;
441 }
442 }
443 }
444 }
445
446 fn double_directory(&mut self) {
447 assert!(
448 self.global_depth < MAX_DEPTH,
449 "the directory has run out of hash bits"
450 );
451 yo_alloc::allow(|| {
452 let mut next = Vec::with_capacity(self.dir.len() * 2);
453 for &s in &self.dir {
454 next.push(s);
455 next.push(s);
456 }
457 self.dir = next;
458 });
459 self.global_depth += 1;
460 self.doublings += 1;
461 }
462
463 /// Every address in the index, in no particular order.
464 ///
465 /// For compaction, which walks the index rather than the arena because an
466 /// allocation has exactly one referent and that referent is an index entry
467 /// (`05` section 3.2).
468 pub fn addresses(&self) -> impl Iterator<Item = Addr> + '_ {
469 self.segs
470 .iter()
471 .enumerate()
472 .flat_map(move |(si, seg)| {
473 // A segment can be named by several directory entries, but it
474 // is visited once here because we walk segments, not the
475 // directory.
476 let _ = si;
477 seg.buckets.iter().chain(seg.overflow.iter())
478 })
479 .flat_map(|b| {
480 (0..SLOTS).filter_map(move |i| {
481 if b.tag(i) == crate::bucket::EMPTY {
482 None
483 } else {
484 Some(b.addr(i))
485 }
486 })
487 })
488 }
489
490 /// Addresses from one segment picked at random, until `out` says stop.
491 ///
492 /// Eviction does not need a fair sample and it cannot afford a real one. It
493 /// needs a handful of keys that are not correlated with each other, quickly,
494 /// and it runs again in a moment if the handful was a bad one. Redis picks a
495 /// random slot in its table and takes a run of consecutive ones from there.
496 /// This is the same idea against a different shape: one segment, then a run
497 /// of consecutive buckets from a random start inside it.
498 ///
499 /// `r` is one draw from the caller's generator and both coordinates come out
500 /// of it, the segment from the top half and the bucket from the bottom.
501 /// Splitting one number rather than asking for two is worth it because this
502 /// is called in a loop and the generator is the same one `SPOP` uses.
503 ///
504 /// The segment is picked uniformly rather than by walking in from a random
505 /// prefix, and that is the whole reason this is not just [`Index::scan`]
506 /// from a made up cursor. A prefix picked uniformly lands in a segment in
507 /// proportion to how much of the prefix space that segment covers, and a
508 /// segment that has never split covers a great deal of it while holding no
509 /// more keys than any other. Sampling that way would look at the keys in
510 /// shallow segments over and over and barely ever look at the rest. Segments
511 /// all split at the same fullness, so picking between them evenly is close
512 /// to picking between keys evenly, which is as close as this needs to get.
513 ///
514 /// Walking forward through the segment rather than stopping at the first
515 /// bucket is what makes this work on a sparse map. A bucket holds seven
516 /// entries and a segment holds sixty four buckets, so a database with two
517 /// keys in it has two buckets that are worth looking in and sixty two that
518 /// are not, and a sampler that gave up after one would come back with
519 /// nothing almost every time. Walking on costs nothing when the map is full,
520 /// because the first bucket already answers.
521 ///
522 /// How many it hands over is the caller's decision and not an argument, which
523 /// is what `out` answering false is for. A count here would be the wrong
524 /// number: the caller is filtering, and under a `volatile` policy on a
525 /// database of mostly permanent keys it may have to look at forty of them to
526 /// find five it can use. Counting entries handed over rather than entries
527 /// kept would stop the walk at the first bucket and report that there is
528 /// nothing to evict, on a database that has plenty.
529 ///
530 /// The bound is the segment. Whatever the caller does, this looks in each of
531 /// the sixty four buckets at most once and then stops, so a caller that never
532 /// says stop still terminates. It can hand back nothing, when the segment it
533 /// picked is empty, and the caller decides whether that is worth another draw.
534 pub fn sample(&self, r: u64, mut out: impl FnMut(Addr) -> bool) {
535 let seg = &self.segs[(r >> 32) as usize % self.segs.len()];
536 let first = (r as usize) % SEGMENT_BUCKETS;
537 for step in 0..SEGMENT_BUCKETS {
538 let mut b = &seg.buckets[(first + step) % SEGMENT_BUCKETS];
539 loop {
540 for i in 0..SLOTS {
541 if b.tag(i) != crate::bucket::EMPTY && !out(b.addr(i)) {
542 return;
543 }
544 }
545 match b.link() {
546 Some(n) => b = &seg.overflow[(n - 1) as usize],
547 None => break,
548 }
549 }
550 }
551 }
552
553 /// Walk one bucket and its overflow chain, and say where to go next.
554 ///
555 /// This is the step [`Cursor`] exists for, and the reasoning behind the
556 /// number it hands back is in that module rather than here. The short of it
557 /// is that the walk goes in increasing prefix order, a segment covers a
558 /// contiguous run of prefixes, and a key's prefix is a function of its hash
559 /// and does not change when the directory doubles or a segment splits.
560 ///
561 /// One bucket a call and not one segment, because a segment is 64 buckets
562 /// and up to 448 entries, and a client that asked for ten of them should not
563 /// get all of those in one reply. The caller decides how many steps make a
564 /// batch.
565 ///
566 /// A cursor a client made up resumes at whatever it points at, which is what
567 /// Redis does. The alternative is remembering every cursor ever handed out.
568 pub fn scan(&self, from: Cursor, mut out: impl FnMut(Addr)) -> Cursor {
569 let g = u32::from(self.global_depth);
570 let prefix = from.prefix();
571 let bucket = from.bucket();
572 let dir_idx = (prefix >> (PREFIX_BITS - g)) as usize;
573 let seg = &self.segs[self.dir[dir_idx] as usize];
574
575 let mut b = &seg.buckets[bucket];
576 loop {
577 for i in 0..SLOTS {
578 if b.tag(i) != crate::bucket::EMPTY {
579 out(b.addr(i));
580 }
581 }
582 match b.link() {
583 Some(n) => b = &seg.overflow[(n - 1) as usize],
584 None => break,
585 }
586 }
587
588 if bucket + 1 < SEGMENT_BUCKETS {
589 return Cursor::at(prefix, bucket + 1);
590 }
591 // The segment is done, so step over the whole run of prefixes it covers
592 // rather than over the one the cursor happens to name. Its local depth
593 // is what says how wide that run is, and rounding down to the start of
594 // the run first is what keeps this correct after a doubling that
595 // happened while the client was away.
596 let span = 1u64 << (PREFIX_BITS - u32::from(seg.local_depth));
597 Cursor::at((prefix & !(span - 1)) + span, 0)
598 }
599
600 /// Replace the address of an entry that is being moved by compaction.
601 ///
602 /// Since the shard owns both the arena and the index, rewriting an index
603 /// entry is a store, which is the whole reason compaction is affordable.
604 pub fn relocate<K: Keys>(&mut self, hash: u64, key: &[u8], to: Addr, keys: &K) -> bool {
605 let tag = tag_of(hash);
606 let seg_idx = self.dir[self.dir_index(hash)] as usize;
607 let bucket_idx = Self::bucket_index(hash);
608 let mut cursor: Option<usize> = None;
609 loop {
610 let found = {
611 let seg = &self.segs[seg_idx];
612 let b = match cursor {
613 None => &seg.buckets[bucket_idx],
614 Some(o) => &seg.overflow[o],
615 };
616 let mut hit = None;
617 for i in b.match_tag(tag) {
618 if keys.eq_at(b.addr(i), key) {
619 hit = Some(i);
620 break;
621 }
622 }
623 (hit, b.link())
624 };
625 if let (Some(i), _) = found {
626 let seg = &mut self.segs[seg_idx];
627 let b = match cursor {
628 None => &mut seg.buckets[bucket_idx],
629 Some(o) => &mut seg.overflow[o],
630 };
631 b.set_addr(i, to);
632 return true;
633 }
634 match found.1 {
635 Some(n) => cursor = Some((n - 1) as usize),
636 None => return false,
637 }
638 }
639 }
640}
641
642impl Default for Index {
643 fn default() -> Index {
644 Index::new()
645 }
646}