Skip to main content

yo_common/
small.rs

1//! A list that stays on the stack until it does not fit.
2//!
3//! Every multi key command builds a handful of little vectors before it does any
4//! work: the slot each key resolved to, the body each slot points at, the
5//! operands sorted by size, a cursor per operand. Each of those is `k` long,
6//! where `k` is the number of keys the command was given, and `k` is two or
7//! three almost every time. A `SINTER` of two eight member sets does about two
8//! hundred nanoseconds of real work and was paying five mallocs and five frees
9//! on top of it.
10//!
11//! [`Small`] is those vectors without the allocator. Up to `N` elements it is an
12//! array in the caller's frame, and past that it is a `Vec` and behaves exactly
13//! as it did before, so a `SUNIONSTORE` over fifty keys is not made worse to
14//! make the common one better.
15//!
16//! # Why `T: Copy` and why there is no unsafe here
17//!
18//! An inline buffer normally needs `MaybeUninit`, because `[T; N]` has to be
19//! filled with something before the first element is written into it. That means
20//! unsafe, and unsafe in a container means getting `Drop` and panic safety right
21//! for a saving measured in nanoseconds.
22//!
23//! There is no need for any of it here. Everything this holds is `Copy`: a slot
24//! number, a shared reference to a body, an index, a cursor over a sorted array.
25//! So the buffer is filled with a copy of the first element and the elements
26//! after `len` are that first element again, harmlessly. Nothing is ever read
27//! out of them, nothing is ever dropped, and the whole type is ordinary safe
28//! Rust.
29//!
30//! [`Small::Empty`] is a variant of its own for the same reason: an inline
31//! buffer needs a value to fill itself with, and a list that never saw a `T` has
32//! not got one.
33//!
34//! # What it is worth
35//!
36//! `yo-kv`'s `setops_small` bench, nanoseconds per operation over sets of eight
37//! and sixty four members, before and after the three vectors inside
38//! `yo_kv::setops` became this:
39//!
40//! ```text
41//!                     before    after
42//!   inter ints k=2     69.50    44.23
43//!   inter ints k=3     88.34    58.28
44//!   union ints k=2     80.39    69.45
45//!   union ints k=3    122.08   114.71
46//!   inter text k=2    160.58   154.84
47//!   union text k=2    370.17   368.54
48//! ```
49//!
50//! The integer intersection is the row that shows it, at about 1.5 times,
51//! because a merge over small sorted arrays is a few dozen nanoseconds of real
52//! work and three allocator round trips were most of what it was doing. The text
53//! rows barely move, because those plans build a hash table sized by the members
54//! and that is what they spend their time on.
55//!
56//! The bench calls `setops` directly, so it does not see the two more vectors
57//! `Keyspace::set_slots` and `Keyspace::bodies_of` used to build per command. A
58//! whole `SINTER` over three small sets went from eleven allocations to none.
59
60use std::ops::{Deref, DerefMut};
61
62/// A list of up to `N` elements on the stack, spilling to the heap past that.
63#[derive(Debug, Clone)]
64pub enum Small<T: Copy, const N: usize> {
65    /// Nothing at all. See the module doc for why this is not `Inline` with a
66    /// length of zero.
67    Empty,
68    /// The first `len` of `buf`. The rest are copies of the first element.
69    Inline {
70        /// The elements, and then padding that is never read.
71        buf: [T; N],
72        /// How many of `buf` are real.
73        len: usize,
74    },
75    /// More than `N` of them, so the allocator was the right answer after all.
76    Spilled(Vec<T>),
77}
78
79impl<T: Copy, const N: usize> Small<T, N> {
80    /// An empty one.
81    #[must_use]
82    pub fn new() -> Small<T, N> {
83        const { assert!(N > 0, "a Small with no inline room is just a Vec") };
84        Small::Empty
85    }
86
87    /// Add one on the end.
88    ///
89    /// Crossing `N` copies what is already there into a `Vec` and never comes
90    /// back, which is the whole of the spill and the reason [`Small::is_inline`]
91    /// is a fact about the list rather than about its length.
92    pub fn push(&mut self, v: T) {
93        const { assert!(N > 0, "a Small with no inline room is just a Vec") };
94        match self {
95            Small::Empty => {
96                *self = Small::Inline {
97                    buf: [v; N],
98                    len: 1,
99                }
100            }
101            Small::Inline { buf, len } if *len < N => {
102                buf[*len] = v;
103                *len += 1;
104            }
105            Small::Inline { buf, len } => {
106                let mut spill = Vec::with_capacity(N * 2);
107                spill.extend_from_slice(&buf[..*len]);
108                spill.push(v);
109                *self = Small::Spilled(spill);
110            }
111            Small::Spilled(s) => s.push(v),
112        }
113    }
114
115    /// Everything the iterator yields, on the stack if it fits.
116    ///
117    /// The spill is decided by the `N` and first element, so an iterator that
118    /// yields `N + 1` copies once and moves everything already collected into a
119    /// `Vec`. That copy is `N` elements of a `Copy` type and is not worth
120    /// avoiding with a size hint that an iterator is allowed to lie about.
121    pub fn collect<I: IntoIterator<Item = T>>(it: I) -> Small<T, N> {
122        const { assert!(N > 0, "a Small with no inline room is just a Vec") };
123        let mut it = it.into_iter();
124        let Some(first) = it.next() else {
125            return Small::Empty;
126        };
127        let mut buf = [first; N];
128        let mut len = 1;
129        while let Some(v) = it.next() {
130            if len == N {
131                let mut spill = Vec::with_capacity(N * 2);
132                spill.extend_from_slice(&buf[..len]);
133                spill.push(v);
134                spill.extend(it);
135                return Small::Spilled(spill);
136            }
137            buf[len] = v;
138            len += 1;
139        }
140        Small::Inline { buf, len }
141    }
142
143    /// Everything in a slice, on the stack if it fits.
144    ///
145    /// [`Small::collect`] has to ask whether it has run out of room on every
146    /// element, because an iterator is not obliged to say how many it has. A
147    /// slice does say, so this asks once and then copies, which is a `memcpy`
148    /// and not a loop. That is worth having wherever the elements are already
149    /// laid out, and a fixed size key built in a local array is the case it was
150    /// written for.
151    #[must_use]
152    pub fn from_slice(s: &[T]) -> Small<T, N> {
153        const { assert!(N > 0, "a Small with no inline room is just a Vec") };
154        let Some(&first) = s.first() else {
155            return Small::Empty;
156        };
157        if s.len() > N {
158            return Small::Spilled(s.to_vec());
159        }
160        let mut buf = [first; N];
161        buf[..s.len()].copy_from_slice(s);
162        Small::Inline { buf, len: s.len() }
163    }
164
165    /// Add a slice on the end, in one go.
166    ///
167    /// Pushing one at a time re-reads which variant this is on every element,
168    /// and the compiler cannot keep the length in a register across it. This
169    /// works out where everything is going first, so the common case is a
170    /// `memcpy` into the inline buffer and the spill happens at most once.
171    pub fn extend_from_slice(&mut self, s: &[T]) {
172        const { assert!(N > 0, "a Small with no inline room is just a Vec") };
173        if s.is_empty() {
174            return;
175        }
176        match self {
177            Small::Empty => *self = Small::from_slice(s),
178            Small::Inline { buf, len } if *len + s.len() <= N => {
179                buf[*len..*len + s.len()].copy_from_slice(s);
180                *len += s.len();
181            }
182            Small::Inline { buf, len } => {
183                let mut spill = Vec::with_capacity((*len + s.len()).max(N * 2));
184                spill.extend_from_slice(&buf[..*len]);
185                spill.extend_from_slice(s);
186                *self = Small::Spilled(spill);
187            }
188            Small::Spilled(v) => v.extend_from_slice(s),
189        }
190    }
191
192    /// The elements, in order.
193    #[must_use]
194    pub fn as_slice(&self) -> &[T] {
195        match self {
196            Small::Empty => &[],
197            Small::Inline { buf, len } => &buf[..*len],
198            Small::Spilled(v) => v,
199        }
200    }
201
202    /// The same, to be sorted or stepped through.
203    pub fn as_mut_slice(&mut self) -> &mut [T] {
204        match self {
205            Small::Empty => &mut [],
206            Small::Inline { buf, len } => &mut buf[..*len],
207            Small::Spilled(v) => v,
208        }
209    }
210
211    /// Whether this one is still on the stack, which is what the tests check
212    /// and what nothing else has any business asking.
213    #[must_use]
214    pub fn is_inline(&self) -> bool {
215        !matches!(self, Small::Spilled(_))
216    }
217}
218
219// Written out rather than derived, whatever clippy thinks. `#[derive(Default)]`
220// on an enum puts a `T: Default` bound on the whole thing, and the whole point
221// of this type is holding references to bodies, which have no default.
222#[allow(clippy::derivable_impls)]
223impl<T: Copy, const N: usize> Default for Small<T, N> {
224    fn default() -> Small<T, N> {
225        Small::Empty
226    }
227}
228
229impl<T: Copy, const N: usize> Deref for Small<T, N> {
230    type Target = [T];
231
232    fn deref(&self) -> &[T] {
233        self.as_slice()
234    }
235}
236
237impl<T: Copy, const N: usize> DerefMut for Small<T, N> {
238    fn deref_mut(&mut self) -> &mut [T] {
239        self.as_mut_slice()
240    }
241}
242
243impl<'a, T: Copy, const N: usize> IntoIterator for &'a Small<T, N> {
244    type Item = &'a T;
245    type IntoIter = std::slice::Iter<'a, T>;
246
247    fn into_iter(self) -> std::slice::Iter<'a, T> {
248        self.as_slice().iter()
249    }
250}
251
252impl<'a, T: Copy, const N: usize> IntoIterator for &'a mut Small<T, N> {
253    type Item = &'a mut T;
254    type IntoIter = std::slice::IterMut<'a, T>;
255
256    fn into_iter(self) -> std::slice::IterMut<'a, T> {
257        self.as_mut_slice().iter_mut()
258    }
259}
260
261impl<T: Copy, const N: usize> FromIterator<T> for Small<T, N> {
262    fn from_iter<I: IntoIterator<Item = T>>(it: I) -> Small<T, N> {
263        Small::collect(it)
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270
271    #[test]
272    fn an_empty_one_is_an_empty_slice() {
273        let s: Small<u32, 4> = Small::new();
274        assert!(s.is_empty());
275        assert_eq!(&*s, &[] as &[u32]);
276        assert!(s.is_inline());
277    }
278
279    #[test]
280    fn everything_up_to_n_stays_on_the_stack() {
281        for n in 1..=4usize {
282            let s: Small<u32, 4> = Small::collect(0..n as u32);
283            assert!(s.is_inline(), "{n} elements spilled and should not have");
284            assert_eq!(&*s, &(0..n as u32).collect::<Vec<_>>()[..]);
285        }
286    }
287
288    #[test]
289    fn one_past_n_spills_and_keeps_everything() {
290        let s: Small<u32, 4> = Small::collect(0..5);
291        assert!(!s.is_inline(), "five in a four did not spill");
292        assert_eq!(&*s, &[0, 1, 2, 3, 4]);
293    }
294
295    /// The spill copies what it already had and then drains the rest of the
296    /// iterator, which is the one place an element could go missing.
297    #[test]
298    fn a_long_spill_keeps_the_order() {
299        let s: Small<u32, 4> = Small::collect(0..1_000);
300        assert!(!s.is_inline());
301        assert_eq!(s.len(), 1_000);
302        assert!(s.iter().copied().eq(0..1_000));
303    }
304
305    /// The two slice forms have to agree with the iterator form at every
306    /// length, either side of the spill, because they are the same list built a
307    /// faster way and not a second type.
308    #[test]
309    fn the_slice_forms_agree_with_collecting() {
310        for n in 0..12usize {
311            let want: Vec<u32> = (0..n as u32).collect();
312
313            let s: Small<u32, 4> = Small::from_slice(&want);
314            assert_eq!(&*s, &want[..], "from_slice at {n}");
315            assert_eq!(s.is_inline(), n <= 4, "from_slice spilled wrongly at {n}");
316
317            for split in 0..=n {
318                let mut s: Small<u32, 4> = Small::from_slice(&want[..split]);
319                s.extend_from_slice(&want[split..]);
320                assert_eq!(&*s, &want[..], "extend at {n} split at {split}");
321            }
322        }
323    }
324
325    /// Extending a list that has already spilled goes to the `Vec` and stays
326    /// there, and extending by nothing at all leaves an empty one empty rather
327    /// than making it inline with no elements.
328    #[test]
329    fn extending_past_the_spill_keeps_everything() {
330        let mut s: Small<u32, 4> = Small::collect(0..6);
331        s.extend_from_slice(&[6, 7, 8]);
332        assert!(!s.is_inline());
333        assert!(s.iter().copied().eq(0..9));
334
335        let mut empty: Small<u32, 4> = Small::new();
336        empty.extend_from_slice(&[]);
337        assert!(empty.is_empty());
338        assert!(matches!(empty, Small::Empty));
339    }
340
341    #[test]
342    fn it_can_be_sorted_in_place_either_way_round() {
343        let mut small: Small<u32, 4> = Small::collect([3, 1, 2]);
344        small.sort_unstable();
345        assert_eq!(&*small, &[1, 2, 3]);
346
347        let mut big: Small<u32, 4> = Small::collect([9, 3, 1, 2, 7, 5]);
348        big.sort_unstable();
349        assert_eq!(&*big, &[1, 2, 3, 5, 7, 9]);
350    }
351
352    /// References are the point of the type, so they get their own case.
353    #[test]
354    fn it_holds_references() {
355        let owned = [1u32, 2, 3];
356        let s: Small<&u32, 4> = owned.iter().collect();
357        assert_eq!(s.iter().copied().copied().collect::<Vec<_>>(), [1, 2, 3]);
358    }
359
360    /// The padding past `len` is a copy of the first element and is never read.
361    /// Nothing depends on that being true, but it is worth pinning down that a
362    /// short list does not accidentally expose it.
363    #[test]
364    fn the_padding_is_not_part_of_the_slice() {
365        let s: Small<u32, 8> = Small::collect([7, 8]);
366        assert_eq!(&*s, &[7, 8]);
367        assert_eq!(s.len(), 2);
368    }
369}