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}