equix/bucket_array/mem.rs
1//! Memory management internals for the bucket array
2//!
3//! The solver needs sparse arrays of sorting buckets as temporary space. To
4//! minimize peak memory usage, we need to dynamically reallocate portions
5//! of that space between usages. A naive solution could use a separate Vec for
6//! each array of bucket keys/values. A quick benchmark confirms that this is
7//! between 2% and 10% worse than if the solver could use a single block of
8//! memory with predetermined packed layout.
9//!
10//! Using a static layout presents some challenges in initialization, since the
11//! entire struct is now too large to fit on the stack reliably. Creating an
12//! instance using Box::new() would cause a stack overflow.
13//!
14//! We can solve several of these problems by orienting the data structure
15//! around MaybeUninit memory: No initialization needed, we can easily switch
16//! between space layouts using a union without risk of transmuting live values,
17//! and we avoid an additional redundant zeroing of memory.
18//!
19//! This module provides a trait, [`Uninit`], for memory that we can safely
20//! treat as uninitialized when it isn't being used. We track that use via a
21//! mutable reference, so that gives us mutually exclusive access enforced at
22//! compile-time.
23//!
24//! That uninitialized memory can be used to build backing layouts that are
25//! made from instances of [`BucketArrayMemory`]. The whole backing layout is
26//! then instantiated using freshly allocated uninitialized memory.
27//!
28//! A [`BucketArrayMemory`] can be temporarily, using a mutable reference,
29//! wrapped into a [`BucketArray`] or half of a [`BucketArrayPair`].
30//! Internally, a [`BucketState`] tracks how many items are in each bucket.
31//! The only supported write operation is appending to an underfull bucket.
32//! Once initialized by such a write, bucket items may be randomly accessed
33//! via the methods on the [`BucketArray`] or [`BucketArrayPair`].
34//!
35//! It's critical for memory safety that we only read from a [`MaybeUninit`]
36//! that has definitely been initialized. The static lifetimes of the mutable
37//! reference and our `counts` array ensure we always start out with zeroed
38//! counts and fully assumed-uninitialized memory. Our write method must be
39//! certain to never increment the counter without actually writing to the
40//! [`MaybeUninit`]. See [`BucketState::insert`].
41
42// We need to allow this warning because we conditionally make some private
43// functions public, but their documentation links to private types.
44#![allow(rustdoc::private_intra_doc_links)]
45
46use num_traits::{One, Zero};
47use std::alloc;
48use std::mem::MaybeUninit;
49use std::ops::{Add, Range};
50
51/// Marker trait for types that are normally assumed uninitialized
52///
53/// # Safety
54///
55/// By implementing this trait, it's a guarantee that uninitialized memory
56/// may be transmuted into this type safely. It implies that the type is `Copy`.
57/// It should contain no bits except for [`MaybeUninit`] fields. Structs and
58/// arrays made entirely of [`MaybeUninit`] are fine.
59///
60/// This memory is always assumed to be uninitialized unless we hold a mutable
61/// reference that's associated with information about specific fields that
62/// were initialized during the reference's lifetime.
63#[cfg_attr(feature = "bucket-array", visibility::make(pub))]
64pub(crate) unsafe trait Uninit: Copy {
65 /// Allocate new uninitialized memory, returning a new Box.
66 fn alloc() -> Box<Self> {
67 // SAFETY: Any type implementing Uninit guarantees that creating an
68 // instance from uninitialized memory is sound. We pass this
69 // pointer's ownership immediately to Box.
70 unsafe {
71 let layout = alloc::Layout::new::<Self>();
72 let ptr: *mut Self = std::mem::transmute(alloc::alloc(layout));
73 Box::from_raw(ptr)
74 }
75 }
76}
77
78/// Backing memory for a single key or value bucket array
79///
80/// Describes `N` buckets which each hold at most `M` items of type `T`.
81///
82/// Implements [`Uninit`]. Structs and unions made from `BucketArrayMemory`
83/// can be soundly marked as [`Uninit`].
84#[cfg_attr(feature = "bucket-array", visibility::make(pub))]
85#[derive(Copy, Clone)]
86pub(crate) struct BucketArrayMemory<
87 // Number of buckets
88 const N: usize,
89 // Number of item slots in each bucket
90 const M: usize,
91 // Item type
92 T: Copy,
93>([[MaybeUninit<T>; M]; N]);
94
95/// Implements the [`Uninit`] trait. This memory is always assumed to be
96/// uninitialized unless we hold a mutable reference paired with bucket
97/// usage information.
98///
99/// SAFETY: We can implement [`Uninit`] for [`BucketArrayMemory`]
100/// because it's `Copy` and it contains only `MaybeUninit` bits.
101unsafe impl<const N: usize, const M: usize, T: Copy> Uninit for BucketArrayMemory<N, M, T> {}
102
103/// Types that can be used as a count of items in a bucket
104#[cfg_attr(feature = "bucket-array", visibility::make(pub))]
105pub(crate) trait Count: Copy + Zero + One + Into<usize> + Add<Self, Output = Self> {}
106
107impl<T: Copy + Zero + One + Into<usize> + Add<Self, Output = Self>> Count for T {}
108
109/// Common implementation for key/value and value-only bucket arrays
110///
111/// Tracks the number of items in each bucket. This is used by [`BucketArray`]
112/// and [`BucketArrayPair`] implementations to safely access only initialized
113/// items in the [`BucketArrayMemory`].
114struct BucketState<
115 // Number of buckets
116 const N: usize,
117 // Maximum number of items per bucket
118 const CAP: usize,
119 // Type for bucket item counts
120 C: Count,
121> {
122 /// Number of initialized items in each bucket
123 ///
124 /// Each bucket B's valid item range would be `(0 .. counts[B])`
125 counts: [C; N],
126}
127
128impl<const N: usize, const CAP: usize, C: Count> BucketState<N, CAP, C> {
129 /// Create a new counter store.
130 ///
131 /// This will happen inside the lifetime of our mutable reference
132 /// to the backing store memory.
133 fn new() -> Self {
134 Self {
135 counts: [C::zero(); N],
136 }
137 }
138
139 /// Append a new item to a specific bucket using a writer callback.
140 ///
141 /// The writer is invoked with an item index, after checking
142 /// bucket capacity but before marking the new item as written.
143 ///
144 /// The writer must unconditionally write to the index it's given, in
145 /// each of the backing memories covered by this state tracker.
146 #[inline(always)]
147 fn insert<F: FnMut(usize)>(&mut self, bucket: usize, mut writer: F) -> Result<(), ()> {
148 let item_count = self.counts[bucket];
149 let count_usize: usize = item_count.into();
150 if count_usize < CAP {
151 writer(count_usize);
152 self.counts[bucket] = item_count + C::one();
153 Ok(())
154 } else {
155 Err(())
156 }
157 }
158
159 /// Look up the valid item range for a particular bucket.
160 ///
161 /// Panics if the bucket index is out of range. Item indices inside the
162 /// returned range are initialized, and any outside may be uninitialized.
163 #[inline(always)]
164 fn item_range(&self, bucket: usize) -> Range<usize> {
165 0..self.counts[bucket].into()
166 }
167}
168
169/// Concrete binding between one [`BucketState`] and one [`BucketArrayMemory`]
170#[cfg_attr(feature = "bucket-array", visibility::make(pub))]
171pub(crate) struct BucketArray<
172 // Lifetime for mutable reference to the backing memory
173 'a,
174 // Number of buckets
175 const N: usize,
176 // Maximum number of items per bucket
177 const CAP: usize,
178 // Type for bucket item counts
179 C: Count,
180 // Item type
181 A: Copy,
182> {
183 /// Reference to external backing memory for type A
184 mem: &'a mut BucketArrayMemory<N, CAP, A>,
185 /// Tracking for which items are in use within each bucket
186 state: BucketState<N, CAP, C>,
187}
188
189impl<'a, const N: usize, const CAP: usize, C: Count, A: Copy> BucketArray<'a, N, CAP, C, A> {
190 /// A new [`BucketArray`] wraps a new [`BucketState`] and some possibly-recycled [`BucketArrayMemory`]
191 #[cfg_attr(feature = "bucket-array", visibility::make(pub))]
192 pub(crate) fn new(mem: &'a mut BucketArrayMemory<N, CAP, A>) -> Self {
193 Self {
194 mem,
195 state: BucketState::new(),
196 }
197 }
198
199 /// Look up the valid item range for a particular bucket.
200 ///
201 /// Panics if the bucket index is out of range.
202 #[cfg_attr(feature = "bucket-array", visibility::make(pub))]
203 #[inline(always)]
204 pub(crate) fn item_range(&self, bucket: usize) -> Range<usize> {
205 self.state.item_range(bucket)
206 }
207
208 /// Look up the value of one item in one bucket.
209 ///
210 /// Panics if the indices are out of range.
211 #[cfg_attr(feature = "bucket-array", visibility::make(pub))]
212 #[inline(always)]
213 pub(crate) fn item_value(&self, bucket: usize, item: usize) -> A {
214 assert!(self.state.item_range(bucket).contains(&item));
215 // SAFETY: This requires that our [`BucketState`] instance accurately
216 // represents which fields in [`mem`] have been initialized.
217 unsafe { self.mem.0[bucket][item].assume_init() }
218 }
219
220 /// Append a new item to a bucket.
221 ///
222 /// If the bucket is full, returns `Err(())` and makes no changes.
223 #[cfg_attr(
224 feature = "bucket-array",
225 visibility::make(pub),
226 allow(clippy::result_unit_err)
227 )]
228 #[inline(always)]
229 pub(crate) fn insert(&mut self, bucket: usize, value: A) -> Result<(), ()> {
230 self.state.insert(bucket, |item| {
231 self.mem.0[bucket][item].write(value);
232 })
233 }
234}
235
236/// Concrete binding between one [`BucketState`] and a pair of [`BucketArrayMemory`]
237#[cfg_attr(feature = "bucket-array", visibility::make(pub))]
238pub(crate) struct BucketArrayPair<
239 // Lifetime for mutable reference to the first backing memory
240 'a,
241 // Lifetime for mutable reference to the second backing memory
242 'b,
243 // Number of buckets
244 const N: usize,
245 // Maximum number of items per bucket
246 const CAP: usize,
247 // Type for bucket item counts
248 C: Count,
249 // Type for items in the first backing memory
250 A: Copy,
251 // Type for items in the second backing memory
252 B: Copy,
253> {
254 /// Reference to external backing memory for type `A`
255 mem_a: &'a mut BucketArrayMemory<N, CAP, A>,
256 /// Reference to external backing memory for type `B`
257 mem_b: &'b mut BucketArrayMemory<N, CAP, B>,
258 /// Tracking for which items are in use within each bucket
259 state: BucketState<N, CAP, C>,
260}
261
262impl<'a, 'b, const N: usize, const CAP: usize, C: Count, A: Copy, B: Copy>
263 BucketArrayPair<'a, 'b, N, CAP, C, A, B>
264{
265 /// A new [`BucketArray`] wraps a new [`BucketState`] and two [`BucketArrayMemory`]
266 #[cfg_attr(feature = "bucket-array", visibility::make(pub))]
267 pub(crate) fn new(
268 mem_a: &'a mut BucketArrayMemory<N, CAP, A>,
269 mem_b: &'b mut BucketArrayMemory<N, CAP, B>,
270 ) -> Self {
271 Self {
272 mem_a,
273 mem_b,
274 state: BucketState::new(),
275 }
276 }
277
278 /// Look up the valid item range for a particular bucket.
279 ///
280 /// Panics if the bucket index is out of range.
281 #[cfg_attr(feature = "bucket-array", visibility::make(pub))]
282 #[inline(always)]
283 pub(crate) fn item_range(&self, bucket: usize) -> Range<usize> {
284 self.state.item_range(bucket)
285 }
286
287 /// Look up the first value for one item in one bucket.
288 ///
289 /// Panics if the indices are out of range.
290 #[cfg_attr(feature = "bucket-array", visibility::make(pub))]
291 #[inline(always)]
292 pub(crate) fn item_value_first(&self, bucket: usize, item: usize) -> A {
293 assert!(self.state.item_range(bucket).contains(&item));
294 // SAFETY: This requires that our [`BucketState`] instance accurately
295 // represents which fields in [`mem`] have been initialized.
296 unsafe { self.mem_a.0[bucket][item].assume_init() }
297 }
298
299 /// Look up the second value for one item in one bucket.
300 ///
301 /// Panics if the indices are out of range.
302 #[cfg_attr(feature = "bucket-array", visibility::make(pub))]
303 #[inline(always)]
304 pub(crate) fn item_value_second(&self, bucket: usize, item: usize) -> B {
305 assert!(self.state.item_range(bucket).contains(&item));
306 // SAFETY: This requires that our [`BucketState`] instance accurately
307 // represents which fields in [`mem`] have been initialized.
308 unsafe { self.mem_b.0[bucket][item].assume_init() }
309 }
310
311 /// Append a new item pair to a bucket.
312 ///
313 /// If the bucket is full, returns Err(()) and makes no changes.
314 #[cfg_attr(
315 feature = "bucket-array",
316 visibility::make(pub),
317 allow(clippy::result_unit_err)
318 )]
319 #[inline(always)]
320 pub(crate) fn insert(&mut self, bucket: usize, first: A, second: B) -> Result<(), ()> {
321 self.state.insert(bucket, |item| {
322 self.mem_a.0[bucket][item].write(first);
323 self.mem_b.0[bucket][item].write(second);
324 })
325 }
326
327 /// Transfer the [`BucketState`] to a new single [`BucketArray`],
328 /// keeping the second half and dropping the first.
329 #[cfg_attr(feature = "bucket-array", visibility::make(pub))]
330 pub(crate) fn drop_first(self) -> BucketArray<'b, N, CAP, C, B> {
331 BucketArray {
332 mem: self.mem_b,
333 state: self.state,
334 }
335 }
336}