Skip to main content

oxiblas_core/memory/
aligned_vec.rs

1//! Memory management utilities for OxiBLAS.
2//!
3//! This module provides:
4//! - Aligned memory allocation
5//! - Stack-based temporary allocation (StackReq pattern)
6//! - Cache-aware data layout utilities
7//! - Prefetch hints for cache optimization
8//! - Memory pool for temporary allocations
9//! - Custom allocator support via the `Alloc` trait
10
11use core::alloc::Layout;
12use core::mem::{align_of, size_of};
13use core::ptr::NonNull;
14
15#[cfg(not(feature = "std"))]
16use alloc::alloc::handle_alloc_error;
17#[cfg(feature = "std")]
18use std::alloc::handle_alloc_error;
19
20use super::alloc::*;
21
22// =============================================================================
23// AlignedVec - Aligned heap allocation
24// =============================================================================
25
26/// Compile-time check that an alignment const generic is a power of two, as
27/// required by [`core::alloc::Layout::from_size_align`].
28///
29/// Call sites wrap the call in an inline `const { .. }` block (stable since
30/// Rust 1.79) so the assertion is evaluated at monomorphization time: if
31/// `ALIGN` is not a power of two, compilation fails with a clear message
32/// instead of `AlignedVec` producing an invalid `Layout` (or silently
33/// rounding up) the first time it is used.
34const fn assert_align_is_power_of_two<const ALIGN: usize>() {
35    assert!(
36        ALIGN.is_power_of_two(),
37        "AlignedVec: ALIGN const generic parameter must be a power of two"
38    );
39}
40
41/// A vector with guaranteed alignment and custom allocator support.
42///
43/// Unlike `Vec<T>`, this type ensures the underlying buffer is aligned
44/// to at least `ALIGN` bytes, which is required for efficient SIMD operations.
45///
46/// # Type Parameters
47///
48/// - `T`: The element type
49/// - `ALIGN`: The minimum alignment in bytes (default: 64 for cache line alignment)
50/// - `A`: The allocator type (default: `Global`)
51///
52/// # Custom Allocators
53///
54/// You can use a custom allocator by specifying the third type parameter.
55/// Any type implementing the [`Alloc`] trait works; here `MyAlloc` simply
56/// forwards to [`Global`] to keep the example self-contained:
57///
58/// ```
59/// use core::alloc::Layout;
60/// use oxiblas_core::memory::{AlignedVec, Alloc, Global};
61///
62/// // Use global allocator (default)
63/// let vec: AlignedVec<f64> = AlignedVec::zeros(100);
64/// assert_eq!(vec.len(), 100);
65///
66/// // A custom allocator only needs to implement `Alloc`.
67/// #[derive(Clone)]
68/// struct MyAlloc(Global);
69///
70/// // SAFETY: delegates every call unchanged to `Global`, which upholds the
71/// // `Alloc` trait's safety contract.
72/// unsafe impl Alloc for MyAlloc {
73///     fn allocate(&self, layout: Layout) -> *mut u8 {
74///         self.0.allocate(layout)
75///     }
76///     fn allocate_zeroed(&self, layout: Layout) -> *mut u8 {
77///         self.0.allocate_zeroed(layout)
78///     }
79///     unsafe fn deallocate(&self, ptr: *mut u8, layout: Layout) {
80///         unsafe { self.0.deallocate(ptr, layout) }
81///     }
82/// }
83///
84/// let custom_vec: AlignedVec<f64, 64, MyAlloc> =
85///     AlignedVec::zeros_in(100, MyAlloc(Global));
86/// assert_eq!(custom_vec.len(), 100);
87/// ```
88pub struct AlignedVec<T, const ALIGN: usize = DEFAULT_ALIGN, A: Alloc = Global> {
89    ptr: NonNull<T>,
90    len: usize,
91    cap: usize,
92    alloc: A,
93}
94
95// Convenience methods using Global allocator
96impl<T, const ALIGN: usize> AlignedVec<T, ALIGN, Global> {
97    /// Creates a new empty aligned vector.
98    #[inline]
99    pub const fn new() -> Self {
100        // Compile-time proof that `ALIGN` is a power of two, as required by
101        // `core::alloc::Layout`. Evaluated at monomorphization time, so an
102        // invalid `ALIGN` fails to compile rather than panicking (or worse,
103        // producing a malformed `Layout`) the first time an instance of this
104        // type is actually allocated.
105        const { assert_align_is_power_of_two::<ALIGN>() };
106
107        AlignedVec {
108            ptr: NonNull::dangling(),
109            len: 0,
110            cap: 0,
111            alloc: Global,
112        }
113    }
114
115    /// Creates a new aligned vector with the given capacity.
116    pub fn with_capacity(capacity: usize) -> Self {
117        Self::with_capacity_in(capacity, Global)
118    }
119
120    /// Creates a new aligned vector filled with zeros.
121    ///
122    /// This is more efficient than creating and then filling, as it uses
123    /// zeroed allocation.
124    pub fn zeros(len: usize) -> Self
125    where
126        T: bytemuck::Zeroable,
127    {
128        Self::zeros_in(len, Global)
129    }
130
131    /// Creates a new aligned vector filled with a value.
132    pub fn filled(len: usize, value: T) -> Self
133    where
134        T: Clone,
135    {
136        Self::filled_in(len, value, Global)
137    }
138
139    /// Creates a new aligned vector from a slice.
140    pub fn from_slice(slice: &[T]) -> Self
141    where
142        T: Clone,
143    {
144        Self::from_slice_in(slice, Global)
145    }
146}
147
148// Methods that work with any allocator
149impl<T, const ALIGN: usize, A: Alloc> AlignedVec<T, ALIGN, A> {
150    /// Creates a new empty aligned vector with the specified allocator.
151    #[inline]
152    pub fn new_in(alloc: A) -> Self {
153        const { assert_align_is_power_of_two::<ALIGN>() };
154
155        AlignedVec {
156            ptr: NonNull::dangling(),
157            len: 0,
158            cap: 0,
159            alloc,
160        }
161    }
162
163    /// Creates a new aligned vector with the given capacity and allocator.
164    pub fn with_capacity_in(capacity: usize, alloc: A) -> Self {
165        if capacity == 0 {
166            return Self::new_in(alloc);
167        }
168
169        let layout = Self::layout_for(capacity);
170        let ptr = alloc.allocate(layout) as *mut T;
171
172        if ptr.is_null() {
173            handle_alloc_error(layout);
174        }
175
176        AlignedVec {
177            ptr: unsafe { NonNull::new_unchecked(ptr) },
178            len: 0,
179            cap: capacity,
180            alloc,
181        }
182    }
183
184    /// Creates a new aligned vector filled with zeros using the specified allocator.
185    pub fn zeros_in(len: usize, alloc: A) -> Self
186    where
187        T: bytemuck::Zeroable,
188    {
189        if len == 0 {
190            return Self::new_in(alloc);
191        }
192
193        let layout = Self::layout_for(len);
194        let ptr = alloc.allocate_zeroed(layout) as *mut T;
195
196        if ptr.is_null() {
197            handle_alloc_error(layout);
198        }
199
200        AlignedVec {
201            ptr: unsafe { NonNull::new_unchecked(ptr) },
202            len,
203            cap: len,
204            alloc,
205        }
206    }
207
208    /// Creates a new aligned vector filled with a value using the specified allocator.
209    pub fn filled_in(len: usize, value: T, alloc: A) -> Self
210    where
211        T: Clone,
212    {
213        let mut vec = Self::with_capacity_in(len, alloc);
214        for _ in 0..len {
215            vec.push(value.clone());
216        }
217        vec
218    }
219
220    /// Creates a new aligned vector from a slice using the specified allocator.
221    pub fn from_slice_in(slice: &[T], alloc: A) -> Self
222    where
223        T: Clone,
224    {
225        let mut vec = Self::with_capacity_in(slice.len(), alloc);
226        for item in slice {
227            vec.push(item.clone());
228        }
229        vec
230    }
231
232    /// Returns a reference to the allocator.
233    #[inline]
234    pub fn allocator(&self) -> &A {
235        &self.alloc
236    }
237
238    /// Returns the layout for a given capacity.
239    ///
240    /// # Panics
241    ///
242    /// Panics (via [`Self::capacity_overflow`]) if `capacity * size_of::<T>()`
243    /// overflows `usize`, or if the resulting size -- rounded up to
244    /// `ALIGN.max(align_of::<T>())` -- would exceed `isize::MAX` bytes.
245    ///
246    /// A naive `capacity * size_of::<T>()` would silently wrap around on
247    /// overflow in release builds (multiplication overflow checks are
248    /// disabled outside of `debug_assertions`), yielding a small, wrong
249    /// `size` that produces a *successfully allocated but undersized*
250    /// buffer. Callers such as [`Self::with_capacity_in`] would then record
251    /// the huge, un-wrapped `capacity` in `self.cap`, so later writes up to
252    /// that bogus capacity (e.g. via [`Self::push`] past `self.len`) would
253    /// write past the real allocation: a heap-buffer overflow. Using
254    /// `checked_mul` turns that silent memory-corruption bug into a loud,
255    /// immediate panic instead.
256    fn layout_for(capacity: usize) -> Layout {
257        const { assert_align_is_power_of_two::<ALIGN>() };
258
259        let size = match capacity.checked_mul(size_of::<T>()) {
260            Some(size) => size,
261            None => Self::capacity_overflow(),
262        };
263        let align = ALIGN.max(align_of::<T>());
264        match Layout::from_size_align(size, align) {
265            Ok(layout) => layout,
266            Err(_) => Self::capacity_overflow(),
267        }
268    }
269
270    /// Reports that `capacity` does not correspond to a valid, addressable
271    /// [`Layout`] for `T` and aborts via panic.
272    ///
273    /// This mirrors the strategy `alloc::raw_vec::RawVec` uses for oversized
274    /// capacities: rather than proceeding with a silently truncated
275    /// (wrapped) allocation size -- which would desynchronize the vector's
276    /// tracked capacity from its real allocation -- fail loudly and
277    /// immediately. Marked `#[cold]`/`#[inline(never)]` so the (exceedingly
278    /// rare) overflow path does not bloat the hot allocation path, and
279    /// implemented as a named function rather than `.unwrap()`/`.expect()`
280    /// so the panic message is specific to `AlignedVec` and its type/align
281    /// parameters.
282    #[cold]
283    #[inline(never)]
284    fn capacity_overflow() -> ! {
285        panic!(
286            "AlignedVec<{}>: capacity overflow -- requested capacity does not \
287             fit in a valid memory layout (capacity * size_of::<T>() overflows \
288             usize, or exceeds isize::MAX bytes when rounded up to align={ALIGN})",
289            core::any::type_name::<T>()
290        );
291    }
292
293    /// Returns the length of the vector.
294    #[inline]
295    pub fn len(&self) -> usize {
296        self.len
297    }
298
299    /// Returns true if the vector is empty.
300    #[inline]
301    pub fn is_empty(&self) -> bool {
302        self.len == 0
303    }
304
305    /// Returns the capacity of the vector.
306    #[inline]
307    pub fn capacity(&self) -> usize {
308        self.cap
309    }
310
311    /// Returns a pointer to the first element.
312    #[inline]
313    pub fn as_ptr(&self) -> *const T {
314        self.ptr.as_ptr()
315    }
316
317    /// Returns a mutable pointer to the first element.
318    #[inline]
319    pub fn as_mut_ptr(&mut self) -> *mut T {
320        self.ptr.as_ptr()
321    }
322
323    /// Returns a slice of the vector.
324    #[inline]
325    pub fn as_slice(&self) -> &[T] {
326        unsafe { core::slice::from_raw_parts(self.ptr.as_ptr(), self.len) }
327    }
328
329    /// Returns a mutable slice of the vector.
330    #[inline]
331    pub fn as_mut_slice(&mut self) -> &mut [T] {
332        unsafe { core::slice::from_raw_parts_mut(self.ptr.as_ptr(), self.len) }
333    }
334
335    /// Pushes a value onto the vector.
336    ///
337    /// # Panics
338    /// Panics if the vector is at capacity.
339    pub fn push(&mut self, value: T) {
340        if self.len >= self.cap {
341            self.grow();
342        }
343
344        unsafe {
345            self.ptr.as_ptr().add(self.len).write(value);
346        }
347        self.len += 1;
348    }
349
350    /// Pops a value from the vector.
351    pub fn pop(&mut self) -> Option<T> {
352        if self.len == 0 {
353            return None;
354        }
355
356        self.len -= 1;
357        unsafe { Some(self.ptr.as_ptr().add(self.len).read()) }
358    }
359
360    /// Clears the vector.
361    pub fn clear(&mut self) {
362        while self.pop().is_some() {}
363    }
364
365    /// Resizes the vector to the given length.
366    pub fn resize(&mut self, new_len: usize, value: T)
367    where
368        T: Clone,
369    {
370        if new_len > self.len {
371            self.reserve(new_len - self.len);
372            for _ in self.len..new_len {
373                self.push(value.clone());
374            }
375        } else {
376            while self.len > new_len {
377                self.pop();
378            }
379        }
380    }
381
382    /// Reserves capacity for at least `additional` more elements.
383    pub fn reserve(&mut self, additional: usize) {
384        let required = self.len + additional;
385        if required > self.cap {
386            let new_cap = required.max(self.cap * 2).max(8);
387            self.realloc(new_cap);
388        }
389    }
390
391    fn grow(&mut self) {
392        let new_cap = if self.cap == 0 { 8 } else { self.cap * 2 };
393        self.realloc(new_cap);
394    }
395
396    fn realloc(&mut self, new_cap: usize) {
397        let new_layout = Self::layout_for(new_cap);
398        let new_ptr = self.alloc.allocate(new_layout) as *mut T;
399
400        if new_ptr.is_null() {
401            handle_alloc_error(new_layout);
402        }
403
404        // Copy existing data
405        if self.cap > 0 {
406            unsafe {
407                core::ptr::copy_nonoverlapping(self.ptr.as_ptr(), new_ptr, self.len);
408                let old_layout = Self::layout_for(self.cap);
409                self.alloc
410                    .deallocate(self.ptr.as_ptr() as *mut u8, old_layout);
411            }
412        }
413
414        self.ptr = unsafe { NonNull::new_unchecked(new_ptr) };
415        self.cap = new_cap;
416    }
417}
418
419impl<T, const ALIGN: usize, A: Alloc> Drop for AlignedVec<T, ALIGN, A> {
420    fn drop(&mut self) {
421        // Drop all elements
422        for i in 0..self.len {
423            unsafe {
424                core::ptr::drop_in_place(self.ptr.as_ptr().add(i));
425            }
426        }
427
428        // Deallocate
429        if self.cap > 0 {
430            let layout = Self::layout_for(self.cap);
431            unsafe {
432                self.alloc.deallocate(self.ptr.as_ptr() as *mut u8, layout);
433            }
434        }
435    }
436}
437
438impl<T, const ALIGN: usize> Default for AlignedVec<T, ALIGN, Global> {
439    fn default() -> Self {
440        Self::new()
441    }
442}
443
444impl<T: Clone, const ALIGN: usize, A: Alloc> Clone for AlignedVec<T, ALIGN, A> {
445    fn clone(&self) -> Self {
446        Self::from_slice_in(self.as_slice(), self.alloc.clone())
447    }
448}
449
450impl<T, const ALIGN: usize, A: Alloc> core::ops::Deref for AlignedVec<T, ALIGN, A> {
451    type Target = [T];
452
453    fn deref(&self) -> &Self::Target {
454        self.as_slice()
455    }
456}
457
458impl<T, const ALIGN: usize, A: Alloc> core::ops::DerefMut for AlignedVec<T, ALIGN, A> {
459    fn deref_mut(&mut self) -> &mut Self::Target {
460        self.as_mut_slice()
461    }
462}
463
464impl<T, const ALIGN: usize, A: Alloc> core::ops::Index<usize> for AlignedVec<T, ALIGN, A> {
465    type Output = T;
466
467    fn index(&self, index: usize) -> &Self::Output {
468        &self.as_slice()[index]
469    }
470}
471
472impl<T, const ALIGN: usize, A: Alloc> core::ops::IndexMut<usize> for AlignedVec<T, ALIGN, A> {
473    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
474        &mut self.as_mut_slice()[index]
475    }
476}
477
478// Safety: AlignedVec is Send/Sync if T and A are
479unsafe impl<T: Send, const ALIGN: usize, A: Alloc + Send> Send for AlignedVec<T, ALIGN, A> {}
480unsafe impl<T: Sync, const ALIGN: usize, A: Alloc + Sync> Sync for AlignedVec<T, ALIGN, A> {}
481
482#[cfg(test)]
483mod tests {
484    use super::*;
485
486    #[test]
487    fn with_capacity_allocates_correctly_aligned_and_sized_buffer() {
488        const ALIGN: usize = 64;
489        let vec: AlignedVec<f32, ALIGN> = AlignedVec::with_capacity(37);
490
491        assert_eq!(vec.capacity(), 37);
492        assert_eq!(vec.len(), 0);
493        assert_eq!(
494            vec.as_ptr() as usize % ALIGN,
495            0,
496            "buffer must be ALIGN-aligned"
497        );
498    }
499
500    #[test]
501    fn zeros_and_push_round_trip() {
502        let mut vec: AlignedVec<f64> = AlignedVec::zeros(4);
503        assert_eq!(vec.as_slice(), &[0.0, 0.0, 0.0, 0.0]);
504
505        vec.push(1.0);
506        vec.push(2.0);
507        assert_eq!(vec.len(), 6);
508        assert_eq!(&vec.as_slice()[4..], &[1.0, 2.0]);
509    }
510
511    #[test]
512    fn reserve_and_grow_preserve_existing_elements() {
513        let mut vec: AlignedVec<u64> = AlignedVec::with_capacity(2);
514        vec.push(10);
515        vec.push(20);
516        // Forces `grow` -> `realloc` -> `layout_for` with a larger capacity.
517        vec.push(30);
518        vec.reserve(64);
519
520        assert!(vec.capacity() >= 67);
521        assert_eq!(vec.as_slice(), &[10, 20, 30]);
522    }
523
524    // Regression test for: `layout_for` computing
525    // `capacity * size_of::<T>()` with an unchecked multiplication. In a
526    // release build (where integer-overflow checks are compiled out), a
527    // capacity just large enough to overflow `usize` would silently wrap
528    // around to a small `size`, so the allocator would hand back a tiny
529    // buffer while `self.cap` kept recording the huge, un-wrapped capacity
530    // requested by the caller -- a heap-buffer-overflow-in-waiting the
531    // moment anything wrote up to that bogus capacity. It must now fail
532    // loudly via `capacity_overflow` instead.
533    #[test]
534    #[should_panic(expected = "capacity overflow")]
535    fn with_capacity_overflowing_size_panics_instead_of_wrapping() {
536        // For `f64` (8 bytes), `usize::MAX / 2` multiplied by 8 overflows
537        // `usize` by a wide margin, so the old unchecked multiplication
538        // would have wrapped rather than triggering `Layout::from_size_align`'s
539        // own (unrelated) `isize::MAX` check.
540        let huge_capacity = usize::MAX / 2;
541        let _vec: AlignedVec<f64> = AlignedVec::with_capacity(huge_capacity);
542    }
543
544    // Regression test for the same bug reached via `zeros_in`, which builds
545    // its `Layout` the same way as `with_capacity_in`.
546    #[test]
547    #[should_panic(expected = "capacity overflow")]
548    fn zeros_overflowing_size_panics_instead_of_wrapping() {
549        let huge_len = usize::MAX / 2;
550        let _vec: AlignedVec<f64> = AlignedVec::zeros(huge_len);
551    }
552
553    // A capacity that does not overflow the `checked_mul` but whose size,
554    // once rounded up to `ALIGN`, exceeds `isize::MAX` must also be rejected
555    // by `Layout::from_size_align` and surfaced as `capacity_overflow`
556    // rather than propagating an `Err` (or, previously, panicking via a
557    // generic `.expect("Invalid layout")`).
558    #[test]
559    #[should_panic(expected = "capacity overflow")]
560    fn with_capacity_isize_max_exceeded_panics() {
561        // `capacity * size_of::<u8>()` does not overflow `usize` here, but
562        // it does exceed `isize::MAX`, which `Layout::from_size_align`
563        // rejects.
564        let capacity = isize::MAX as usize;
565        let _vec: AlignedVec<u8> = AlignedVec::with_capacity(capacity);
566    }
567}