Skip to main content

nam_rs/math/common/
aligned.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 Fábio Henrique de Lima Silva (fhl.bsb@gmail.com) All rights reserved.
3
4//! Utility for 64-byte aligned memory allocation.
5//!
6//! Ensures that dynamic buffers (weights, accumulators) respect cache line
7//! boundaries and AVX2/AVX-512 requirements, avoiding unaligned load/store
8//! penalties.
9//!
10//! For huge-page-backed allocations (> 1 MiB), see `huge_alloc::HugePageVec`.
11
12use std::alloc::{Layout, alloc, dealloc};
13use std::ops::{Deref, DerefMut};
14use std::ptr::NonNull;
15
16use crate::common::diagnostics::NamErrorCode;
17
18/// Marker trait for types where the all-zero bit pattern is a valid representation.
19///
20/// # Safety
21///
22/// The implementor guarantees that calling `std::mem::zeroed::<Self>()` produces
23/// a well-defined, valid value of `Self`. All IEEE 754 floating-point types and
24/// all primitive integers satisfy this invariant (0.0 is all-zero bits, and 0
25/// is all-zero bits).
26pub unsafe trait Zeroable: Copy {
27    /// Returns `true` when all bytes of this value are zero.
28    fn is_zero(&self) -> bool;
29}
30
31macro_rules! impl_zeroable_int {
32    ($($t:ty),*) => {
33        $(
34            // SAFETY: for all primitive integer types, the all-zero bit pattern
35            // is the canonical representation of the value 0 — this is guaranteed
36            // by the Rust language reference.
37            unsafe impl Zeroable for $t {
38                #[inline(always)]
39                fn is_zero(&self) -> bool {
40                    *self == 0
41                }
42            }
43        )*
44    };
45}
46
47impl_zeroable_int!(i8, i16, i32, i64, isize, u8, u16, u32, u64, usize);
48
49// SAFETY: IEEE 754 represents +0.0 as all-zero bits (`to_bits() == 0`).
50// `std::mem::zeroed::<f32>()` is thus the canonical +0.0, valid for all
51// arithmetic operations.
52unsafe impl Zeroable for f32 {
53    #[inline(always)]
54    fn is_zero(&self) -> bool {
55        self.to_bits() == 0
56    }
57}
58
59// SAFETY: IEEE 754 represents +0.0 as all-zero bits (`to_bits() == 0`).
60// `std::mem::zeroed::<f64>()` is thus the canonical +0.0, valid for all
61// arithmetic operations.
62unsafe impl Zeroable for f64 {
63    #[inline(always)]
64    fn is_zero(&self) -> bool {
65        self.to_bits() == 0
66    }
67}
68
69/// Wrapper to guarantee 64-byte alignment (Cache Line / AVX-512).
70///
71/// Stack-allocated counterpart to `AlignedVec`. Use this for fixed-size
72/// const-generic buffers on struct fields; use `AlignedVec` for dynamic
73/// heap-allocated buffers.
74#[repr(align(64))]
75#[derive(Clone, Copy, Debug)]
76pub struct Aligned64<T>(pub T);
77
78impl<T> Deref for Aligned64<T> {
79    type Target = T;
80    #[inline(always)]
81    fn deref(&self) -> &Self::Target {
82        &self.0
83    }
84}
85
86impl<T> DerefMut for Aligned64<T> {
87    #[inline(always)]
88    fn deref_mut(&mut self) -> &mut Self::Target {
89        &mut self.0
90    }
91}
92
93impl<T: Default> Default for Aligned64<T> {
94    #[inline(always)]
95    fn default() -> Self {
96        Aligned64(T::default())
97    }
98}
99
100/// A 64-byte aligned buffer (Cache Line / AVX-512).
101///
102/// Layout (24 bytes): pointer + length + capacity. Zero overhead for standard allocations.
103/// For huge-page-backed variants, use `huge_alloc::HugePageVec` instead.
104#[derive(Debug)]
105pub struct AlignedVec<T> {
106    ptr: NonNull<T>,
107    len: usize,
108    cap: usize,
109}
110
111impl<T> AlignedVec<T> {
112    /// The default guaranteed alignment (64 bytes).
113    pub const ALIGN: usize = 64;
114
115    /// Allocates a 64-byte aligned block for `capacity` elements.
116    ///
117    /// Returns the raw pointer and the `Layout` used (for later deallocation).
118    /// This is the single allocation entry-point; all constructors route through it.
119    fn alloc_slot(capacity: usize) -> Result<(*mut T, Layout), NamErrorCode> {
120        if capacity == 0 {
121            return Ok((NonNull::dangling().as_ptr(), Layout::new::<()>()));
122        }
123
124        let element_size = std::mem::size_of::<T>();
125        let byte_size = capacity
126            .checked_mul(element_size)
127            .ok_or(NamErrorCode::OutOfMemory)?;
128
129        let layout = Layout::from_size_align(byte_size, Self::ALIGN)
130            .map_err(|_| NamErrorCode::OutOfMemory)?;
131
132        // SAFETY: layout is derived from valid size (capacity * element_size, checked against
133        // overflow) with 64-byte alignment (Self::ALIGN). alloc returns either a valid aligned
134        // pointer or null.
135        let ptr = unsafe { alloc(layout) };
136        if ptr.is_null() {
137            return Err(NamErrorCode::OutOfMemory);
138        }
139
140        Ok((ptr as *mut T, layout))
141    }
142
143    /// Creates a new aligned buffer and fills it with an initial value.
144    ///
145    /// Think of a shelf where each compartment has exactly the size the processor
146    /// prefers to read (64 bytes). This function reserves the space and places a "default value"
147    /// in each spot, leaving everything ready for immediate use.
148    ///
149    /// # Errors
150    /// Returns `NamErrorCode::OutOfMemory` if allocation fails.
151    pub fn new(len: usize, default: T) -> Result<Self, NamErrorCode>
152    where
153        T: Copy + Zeroable,
154    {
155        let mut vec = Self::with_capacity(len)?;
156        // SAFETY: ptr is non-null with cap ≥ len (from with_capacity).
157        // write_bytes sets raw memory — Zeroable guarantees all-zero bits is a valid T.
158        // Element-by-element write for non-zero default: T: Copy prevents double-free,
159        // and each slot receives a valid bitwise copy of default.
160        unsafe {
161            let ptr = vec.ptr.as_ptr();
162            if default.is_zero() {
163                std::ptr::write_bytes(ptr as *mut u8, 0u8, len * std::mem::size_of::<T>());
164            } else {
165                for i in 0..len {
166                    ptr.add(i).write(default);
167                }
168            }
169        }
170        vec.len = len;
171        Ok(vec)
172    }
173
174    /// Reserves memory space with the required alignment, but without filling the data yet.
175    ///
176    /// It's like reserving an exclusive parking lot: the space is there, guaranteed and
177    /// organized in 64-byte blocks, but the "slots" are still empty.
178    /// This special alignment allows the processor to read data at maximum
179    /// speed, without needing to "adjust" the memory position.
180    ///
181    /// # Errors
182    /// Returns `NamErrorCode::OutOfMemory` if allocation fails.
183    pub fn with_capacity(capacity: usize) -> Result<Self, NamErrorCode> {
184        let (ptr, _layout) = Self::alloc_slot(capacity)?;
185        Ok(Self {
186            ptr: NonNull::new(ptr).unwrap_or(NonNull::dangling()),
187            len: 0,
188            cap: capacity,
189        })
190    }
191
192    /// Returns the number of elements in the buffer.
193    pub fn len(&self) -> usize {
194        self.len
195    }
196
197    /// Returns the allocated capacity (max number of elements without reallocation).
198    pub fn cap(&self) -> usize {
199        self.cap
200    }
201
202    /// Returns true if the buffer is empty.
203    pub fn is_empty(&self) -> bool {
204        self.len == 0
205    }
206
207    /// Resizes the buffer to a new length, filling new elements with `default`.
208    /// If `new_len <= self.len`, this is a no-op (never shrinks).
209    ///
210    /// # Errors
211    /// Returns `NamErrorCode::OutOfMemory` if allocation for the new capacity fails.
212    pub fn resize(&mut self, new_len: usize, default: T) -> Result<(), NamErrorCode>
213    where
214        T: Copy + Zeroable,
215    {
216        if new_len <= self.len {
217            return Ok(());
218        }
219        let mut new_vec = Self::with_capacity(new_len)?;
220        // SAFETY: old_ptr is valid for self.len reads (self is initialized).
221        // new_vec.ptr is non-null with cap ≥ new_len (freshly allocated). Source and
222        // destination are separate allocations — no overlap possible.
223        // copy_nonoverlapping moves self.len elements atomically.
224        // write_bytes fills remaining slots with zero bytes; Zeroable guarantees
225        // all-zero bits is a valid T.
226        // Element-by-element write for non-zero default covers remaining slots.
227        unsafe {
228            let old_ptr = self.ptr.as_ptr();
229            let new_ptr = new_vec.ptr.as_ptr();
230            std::ptr::copy_nonoverlapping(old_ptr, new_ptr, self.len);
231            if default.is_zero() {
232                let fill_start = new_ptr.add(self.len) as *mut u8;
233                let fill_bytes = (new_len - self.len) * std::mem::size_of::<T>();
234                std::ptr::write_bytes(fill_start, 0u8, fill_bytes);
235            } else {
236                for i in self.len..new_len {
237                    new_ptr.add(i).write(default);
238                }
239            }
240        }
241        new_vec.len = new_len;
242        let old = std::mem::replace(self, new_vec);
243        drop(old);
244        Ok(())
245    }
246
247    /// An optimized conversion from regular vector to aligned buffer.
248    ///
249    /// Uses a direct memory copy to ensure the transfer is as fast as possible,
250    /// maintaining the 64-byte alignment.
251    ///
252    /// # Errors
253    /// Returns `NamErrorCode::OutOfMemory` if allocation fails.
254    pub fn from_vec(v: Vec<T>) -> Result<Self, NamErrorCode>
255    where
256        T: Copy,
257    {
258        if v.is_empty() {
259            return Self::with_capacity(0);
260        }
261        let mut aligned = Self::with_capacity(v.len())?;
262        // SAFETY: v.as_ptr() is valid for v.len() reads (Vec invariant). aligned.ptr is non-null
263        // with cap = v.len() (freshly allocated by with_capacity). T: Copy guarantees the source
264        // and destination do not overlap (separate allocations), so copy_nonoverlapping is sound.
265        unsafe {
266            std::ptr::copy_nonoverlapping(v.as_ptr(), aligned.ptr.as_ptr(), v.len());
267        }
268        aligned.len = v.len();
269        Ok(aligned)
270    }
271}
272
273/// Allows accessing the buffer data as if it were a regular Rust slice.
274///
275/// This simplifies usage, as you can use functions that expect a normal slice
276/// without needing complicated conversions.
277impl<T> Deref for AlignedVec<T> {
278    type Target = [T];
279
280    fn deref(&self) -> &Self::Target {
281        if self.len == 0 {
282            &[]
283        } else {
284            // SAFETY: self.ptr is non-null from with_capacity (or dangling for zero-cap which is guarded
285            // by the len==0 branch above). self.len ≤ self.cap and all elements [0..len) are initialized.
286            unsafe { std::slice::from_raw_parts(self.ptr.as_ptr(), self.len) }
287        }
288    }
289}
290
291/// Allows accessing and modifying buffer data as a regular list.
292///
293/// Ensures freedom to read and write content in a simple and direct way.
294impl<T> DerefMut for AlignedVec<T> {
295    fn deref_mut(&mut self) -> &mut Self::Target {
296        if self.len == 0 {
297            &mut []
298        } else {
299            // SAFETY: self.ptr is non-null with cap > 0. self.len ≤ self.cap, all elements in [0..len)
300            // are initialized. &mut self guarantees exclusive access — no aliasing possible.
301            unsafe { std::slice::from_raw_parts_mut(self.ptr.as_ptr(), self.len) }
302        }
303    }
304}
305
306/// Ensures memory is returned to the system when the object is destroyed.
307///
308/// It's the "cleaner" that frees the reserved space as soon as you finish using
309/// the buffer, preventing memory waste (so-called "memory leaks").
310impl<T> Drop for AlignedVec<T> {
311    fn drop(&mut self) {
312        if self.cap > 0 {
313            // Layout construction cannot fail here: self.cap was validated during allocation
314            // when alloc_slot produced a valid layout with the same size + alignment.
315            if let Ok(layout) =
316                Layout::from_size_align(self.cap * std::mem::size_of::<T>(), Self::ALIGN)
317            {
318                // SAFETY: self.ptr was allocated by alloc_slot using an identical layout
319                // (size=cap*size_of::<T>(), align=64). The layout is recalculated identically
320                // here. Drop runs exactly once (Rust ownership semantics); all elements have
321                // been properly dropped via DerefMut or leakage.
322                unsafe {
323                    dealloc(self.ptr.as_ptr() as *mut u8, layout);
324                }
325            }
326        }
327    }
328}
329
330/// Creates an identical copy of the buffer and its entire contents.
331///
332/// Reserves new memory space with the same alignment and performs a single
333/// `copy_nonoverlapping` blit — no element-by-element iteration.
334impl<T: Copy> Clone for AlignedVec<T> {
335    fn clone(&self) -> Self {
336        // The layout (self.len * size_of::<T>(), 64) is identical to a layout that was
337        // already validated during the original allocation. Therefore, alloc_slot cannot
338        // return Err here — only a true system OOM could fail, which is unrecoverable.
339        let (ptr, _layout) = Self::alloc_slot(self.len)
340            .expect("clone: layout identical to validated original allocation");
341        let mut new_vec = Self {
342            ptr: NonNull::new(ptr).unwrap_or(NonNull::dangling()),
343            len: 0,
344            cap: self.len,
345        };
346        // SAFETY: self.ptr is valid for self.len reads. new_vec.ptr is non-null with
347        // cap = self.len (freshly allocated). Source and destination are separate
348        // allocations — no overlap possible. T: Copy guarantees that bitwise duplication
349        // is semantically equivalent to Clone::clone.
350        unsafe {
351            std::ptr::copy_nonoverlapping(self.ptr.as_ptr(), new_vec.ptr.as_ptr(), self.len);
352        }
353        new_vec.len = self.len;
354        new_vec
355    }
356}
357
358/// Tells Rust that it is safe to send and share this buffer between different threads.
359///
360/// This is essential so that audio processing can be distributed across
361/// multiple processor cores with full safety.
362// SAFETY: AlignedVec owns its heap allocation exclusively. When T: Send, all elements can be
363// safely transferred across thread boundaries. Destructors run on the receiving thread only.
364unsafe impl<T: Send> Send for AlignedVec<T> {}
365// SAFETY: AlignedVec's heap allocation is never mutated through shared references
366// (Deref provides &[T]). When T: Sync, shared reads of elements across threads are safe.
367unsafe impl<T: Sync> Sync for AlignedVec<T> {}
368
369#[cfg(test)]
370#[path = "aligned_test.rs"]
371mod tests;