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    /// The elements, in order.
144    #[must_use]
145    pub fn as_slice(&self) -> &[T] {
146        match self {
147            Small::Empty => &[],
148            Small::Inline { buf, len } => &buf[..*len],
149            Small::Spilled(v) => v,
150        }
151    }
152
153    /// The same, to be sorted or stepped through.
154    pub fn as_mut_slice(&mut self) -> &mut [T] {
155        match self {
156            Small::Empty => &mut [],
157            Small::Inline { buf, len } => &mut buf[..*len],
158            Small::Spilled(v) => v,
159        }
160    }
161
162    /// Whether this one is still on the stack, which is what the tests check
163    /// and what nothing else has any business asking.
164    #[must_use]
165    pub fn is_inline(&self) -> bool {
166        !matches!(self, Small::Spilled(_))
167    }
168}
169
170// Written out rather than derived, whatever clippy thinks. `#[derive(Default)]`
171// on an enum puts a `T: Default` bound on the whole thing, and the whole point
172// of this type is holding references to bodies, which have no default.
173#[allow(clippy::derivable_impls)]
174impl<T: Copy, const N: usize> Default for Small<T, N> {
175    fn default() -> Small<T, N> {
176        Small::Empty
177    }
178}
179
180impl<T: Copy, const N: usize> Deref for Small<T, N> {
181    type Target = [T];
182
183    fn deref(&self) -> &[T] {
184        self.as_slice()
185    }
186}
187
188impl<T: Copy, const N: usize> DerefMut for Small<T, N> {
189    fn deref_mut(&mut self) -> &mut [T] {
190        self.as_mut_slice()
191    }
192}
193
194impl<'a, T: Copy, const N: usize> IntoIterator for &'a Small<T, N> {
195    type Item = &'a T;
196    type IntoIter = std::slice::Iter<'a, T>;
197
198    fn into_iter(self) -> std::slice::Iter<'a, T> {
199        self.as_slice().iter()
200    }
201}
202
203impl<'a, T: Copy, const N: usize> IntoIterator for &'a mut Small<T, N> {
204    type Item = &'a mut T;
205    type IntoIter = std::slice::IterMut<'a, T>;
206
207    fn into_iter(self) -> std::slice::IterMut<'a, T> {
208        self.as_mut_slice().iter_mut()
209    }
210}
211
212impl<T: Copy, const N: usize> FromIterator<T> for Small<T, N> {
213    fn from_iter<I: IntoIterator<Item = T>>(it: I) -> Small<T, N> {
214        Small::collect(it)
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[test]
223    fn an_empty_one_is_an_empty_slice() {
224        let s: Small<u32, 4> = Small::new();
225        assert!(s.is_empty());
226        assert_eq!(&*s, &[] as &[u32]);
227        assert!(s.is_inline());
228    }
229
230    #[test]
231    fn everything_up_to_n_stays_on_the_stack() {
232        for n in 1..=4usize {
233            let s: Small<u32, 4> = Small::collect(0..n as u32);
234            assert!(s.is_inline(), "{n} elements spilled and should not have");
235            assert_eq!(&*s, &(0..n as u32).collect::<Vec<_>>()[..]);
236        }
237    }
238
239    #[test]
240    fn one_past_n_spills_and_keeps_everything() {
241        let s: Small<u32, 4> = Small::collect(0..5);
242        assert!(!s.is_inline(), "five in a four did not spill");
243        assert_eq!(&*s, &[0, 1, 2, 3, 4]);
244    }
245
246    /// The spill copies what it already had and then drains the rest of the
247    /// iterator, which is the one place an element could go missing.
248    #[test]
249    fn a_long_spill_keeps_the_order() {
250        let s: Small<u32, 4> = Small::collect(0..1_000);
251        assert!(!s.is_inline());
252        assert_eq!(s.len(), 1_000);
253        assert!(s.iter().copied().eq(0..1_000));
254    }
255
256    #[test]
257    fn it_can_be_sorted_in_place_either_way_round() {
258        let mut small: Small<u32, 4> = Small::collect([3, 1, 2]);
259        small.sort_unstable();
260        assert_eq!(&*small, &[1, 2, 3]);
261
262        let mut big: Small<u32, 4> = Small::collect([9, 3, 1, 2, 7, 5]);
263        big.sort_unstable();
264        assert_eq!(&*big, &[1, 2, 3, 5, 7, 9]);
265    }
266
267    /// References are the point of the type, so they get their own case.
268    #[test]
269    fn it_holds_references() {
270        let owned = [1u32, 2, 3];
271        let s: Small<&u32, 4> = owned.iter().collect();
272        assert_eq!(s.iter().copied().copied().collect::<Vec<_>>(), [1, 2, 3]);
273    }
274
275    /// The padding past `len` is a copy of the first element and is never read.
276    /// Nothing depends on that being true, but it is worth pinning down that a
277    /// short list does not accidentally expose it.
278    #[test]
279    fn the_padding_is_not_part_of_the_slice() {
280        let s: Small<u32, 8> = Small::collect([7, 8]);
281        assert_eq!(&*s, &[7, 8]);
282        assert_eq!(s.len(), 2);
283    }
284}