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}