Skip to main content

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}