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}