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;