Skip to main content

scivex_core/
arena.rs

1//! Arena and slab allocators for temporary tensor buffers.
2//!
3//! Provides `Arena` for bump-style allocation of temporary numeric slices,
4//! and `SlabPool` for recycling fixed-size buffers. Both are designed to
5//! eliminate per-allocation heap overhead in hot computation loops.
6
7use std::cell::RefCell;
8use std::marker::PhantomData;
9use std::mem;
10
11/// A simple arena allocator for temporary numeric buffers.
12///
13/// Allocates from a pre-allocated block of memory, avoiding per-allocation
14/// heap overhead. Suitable for temporary tensors in computation loops.
15///
16/// # Examples
17///
18/// ```
19/// use scivex_core::arena::Arena;
20///
21/// let arena = Arena::new(1024); // 1024 f64 slots
22/// let buf1 = arena.alloc::<f64>(100).unwrap();
23/// let buf2 = arena.alloc::<f64>(200).unwrap();
24/// assert_eq!(buf1.len(), 100);
25/// assert_eq!(buf2.len(), 200);
26/// arena.reset(); // reclaim all memory instantly
27/// ```
28pub struct Arena {
29    /// Raw byte storage.
30    data: RefCell<Vec<u8>>,
31    /// Current offset into the data buffer.
32    offset: RefCell<usize>,
33    /// Total capacity in bytes.
34    capacity: usize,
35}
36
37/// A borrowed slice from the arena.
38///
39/// The arena must outlive this handle. Dereferences to `&[T]` / `&mut [T]`
40/// for ergonomic access.
41pub struct ArenaSlice<'a, T> {
42    ptr: *mut T,
43    len: usize,
44    _marker: PhantomData<&'a mut T>,
45}
46
47// SAFETY: ArenaSlice does not implement Send/Sync because Arena uses RefCell,
48// which is !Sync. The PhantomData<&'a mut T> correctly limits the lifetime.
49
50impl Arena {
51    /// Create a new arena with capacity for `n_elements` `f64` values.
52    ///
53    /// The actual byte capacity is `n_elements * size_of::<f64>()`.
54    pub fn new(n_elements: usize) -> Self {
55        Self::with_byte_capacity(n_elements * mem::size_of::<f64>())
56    }
57
58    /// Create an arena with the given byte capacity.
59    pub fn with_byte_capacity(bytes: usize) -> Self {
60        let data = vec![0u8; bytes];
61        Self {
62            data: RefCell::new(data),
63            offset: RefCell::new(0),
64            capacity: bytes,
65        }
66    }
67
68    /// Allocate a slice of `count` elements of type `T` from the arena.
69    ///
70    /// Returns `None` if the arena doesn't have enough space.
71    /// Elements are zero-initialized.
72    pub fn alloc<T: Default + Copy>(&self, count: usize) -> Option<ArenaSlice<'_, T>> {
73        let size = mem::size_of::<T>();
74        let align = mem::align_of::<T>();
75        let total_bytes = size * count;
76
77        let mut offset = self.offset.borrow_mut();
78
79        // Round up to the required alignment.
80        let aligned = (*offset + align - 1) & !(align - 1);
81
82        if aligned + total_bytes > self.capacity {
83            return None;
84        }
85
86        let data = self.data.borrow_mut();
87        let base_ptr = data.as_ptr();
88
89        // SAFETY: `aligned + total_bytes <= self.capacity` is checked above.
90        // The data vec has been allocated and zeroed to `self.capacity` bytes.
91        // The pointer arithmetic stays within the vec's allocation.
92        // We ensure proper alignment by rounding `aligned` up to `align_of::<T>()`.
93        let ptr = unsafe { base_ptr.add(aligned) as *mut T };
94
95        // Zero-initialize the allocated region (may have stale data after reset).
96        // SAFETY: Same bounds justification as above. The region
97        // [aligned..aligned+total_bytes) is within the vec's allocation.
98        unsafe {
99            std::ptr::write_bytes(ptr, 0, count);
100        }
101
102        *offset = aligned + total_bytes;
103
104        Some(ArenaSlice {
105            ptr,
106            len: count,
107            _marker: PhantomData,
108        })
109    }
110
111    /// Allocate a slice and initialize it from an existing slice.
112    ///
113    /// Returns `None` if the arena doesn't have enough space.
114    pub fn alloc_copy<T: Copy>(&self, src: &[T]) -> Option<ArenaSlice<'_, T>> {
115        let _size = mem::size_of::<T>();
116        let align = mem::align_of::<T>();
117        let total_bytes = std::mem::size_of_val(src);
118
119        let mut offset = self.offset.borrow_mut();
120
121        let aligned = (*offset + align - 1) & !(align - 1);
122
123        if aligned + total_bytes > self.capacity {
124            return None;
125        }
126
127        let data = self.data.borrow_mut();
128        let base_ptr = data.as_ptr();
129
130        // SAFETY: `aligned + total_bytes <= self.capacity` is checked above.
131        // The data vec is allocated to at least `self.capacity` bytes.
132        // Alignment is ensured by rounding up to `align_of::<T>()`.
133        let ptr = unsafe { base_ptr.add(aligned) as *mut T };
134
135        // SAFETY: `ptr` points to `src.len()` elements worth of valid,
136        // aligned memory within the arena's allocation. Source and destination
137        // do not overlap because `src` is an external slice.
138        unsafe {
139            std::ptr::copy_nonoverlapping(src.as_ptr(), ptr, src.len());
140        }
141
142        *offset = aligned + total_bytes;
143
144        Some(ArenaSlice {
145            ptr,
146            len: src.len(),
147            _marker: PhantomData,
148        })
149    }
150
151    /// Reset the arena, making all previously allocated memory available again.
152    ///
153    /// This is O(1) — just resets the offset pointer. Previously returned
154    /// `ArenaSlice` handles become invalid (enforced by lifetimes).
155    pub fn reset(&self) {
156        *self.offset.borrow_mut() = 0;
157    }
158
159    /// Returns the number of bytes currently in use.
160    pub fn used_bytes(&self) -> usize {
161        *self.offset.borrow()
162    }
163
164    /// Returns the total capacity in bytes.
165    pub fn capacity_bytes(&self) -> usize {
166        self.capacity
167    }
168
169    /// Returns the number of bytes remaining.
170    pub fn remaining_bytes(&self) -> usize {
171        self.capacity - *self.offset.borrow()
172    }
173}
174
175impl<T> ArenaSlice<'_, T> {
176    /// Returns an immutable slice view of the arena allocation.
177    pub fn as_slice(&self) -> &[T] {
178        // SAFETY: The pointer was obtained from a valid arena allocation of
179        // `self.len` elements. The arena's lifetime is tied to this slice
180        // through the `'a` lifetime parameter, ensuring the memory remains
181        // valid. The elements were properly initialized (zeroed or copied).
182        unsafe { std::slice::from_raw_parts(self.ptr, self.len) }
183    }
184
185    /// Returns a mutable slice view of the arena allocation.
186    pub fn as_mut_slice(&mut self) -> &mut [T] {
187        // SAFETY: Same as `as_slice`, plus we hold `&mut self` guaranteeing
188        // exclusive access to this allocation region.
189        unsafe { std::slice::from_raw_parts_mut(self.ptr, self.len) }
190    }
191
192    /// Returns the number of elements in this slice.
193    pub fn len(&self) -> usize {
194        self.len
195    }
196
197    /// Returns `true` if the slice has zero elements.
198    pub fn is_empty(&self) -> bool {
199        self.len == 0
200    }
201}
202
203impl<T> std::ops::Deref for ArenaSlice<'_, T> {
204    type Target = [T];
205
206    fn deref(&self) -> &[T] {
207        self.as_slice()
208    }
209}
210
211impl<T> std::ops::DerefMut for ArenaSlice<'_, T> {
212    fn deref_mut(&mut self) -> &mut [T] {
213        self.as_mut_slice()
214    }
215}
216
217// ---------------------------------------------------------------------------
218// SlabPool
219// ---------------------------------------------------------------------------
220
221/// Default slab size classes (element counts).
222const DEFAULT_SIZE_CLASSES: &[usize] = &[64, 256, 1024, 4096, 16384, 65536];
223
224/// A pool of fixed-size slabs for common tensor sizes.
225///
226/// Pre-allocates slabs of common sizes (e.g., 64, 256, 1024, 4096 elements)
227/// and hands them out on request, recycling them when returned.
228///
229/// # Examples
230///
231/// ```
232/// use scivex_core::arena::SlabPool;
233///
234/// let pool: SlabPool<f64> = SlabPool::new();
235/// let mut buf = pool.acquire(100); // gets a 256-element slab
236/// assert!(buf.capacity() >= 100);
237/// buf[0] = 42.0;
238/// pool.release(buf);
239/// // The next acquire of a similar size reuses the released buffer.
240/// let buf2 = pool.acquire(100);
241/// assert!(buf2.capacity() >= 100);
242/// ```
243pub struct SlabPool<T: Copy> {
244    /// `slabs[i]` holds available buffers for `size_classes[i]`.
245    slabs: RefCell<Vec<Vec<Vec<T>>>>,
246    /// Sorted list of slab element counts.
247    size_classes: Vec<usize>,
248}
249
250impl<T: Copy + Default> SlabPool<T> {
251    /// Create a new slab pool with default size classes
252    /// (64, 256, 1024, 4096, 16384, 65536).
253    pub fn new() -> Self {
254        Self::with_sizes(DEFAULT_SIZE_CLASSES)
255    }
256
257    /// Create a pool with custom size classes.
258    ///
259    /// The provided sizes are sorted internally.
260    pub fn with_sizes(sizes: &[usize]) -> Self {
261        let mut size_classes: Vec<usize> = sizes.to_vec();
262        size_classes.sort_unstable();
263
264        let slabs = RefCell::new(size_classes.iter().map(|_| Vec::new()).collect());
265
266        Self {
267            slabs,
268            size_classes,
269        }
270    }
271
272    /// Acquire a buffer of at least `min_size` elements.
273    ///
274    /// If a recycled slab of suitable size is available, it is returned
275    /// (zero-cleared). Otherwise a fresh `Vec` is allocated.
276    pub fn acquire(&self, min_size: usize) -> Vec<T> {
277        let class_idx = self.size_class_index(min_size);
278
279        let mut slabs = self.slabs.borrow_mut();
280
281        if let Some(idx) = class_idx {
282            if let Some(mut buf) = slabs[idx].pop() {
283                // Clear for reuse.
284                buf.clear();
285                buf.resize(self.size_classes[idx], T::default());
286                return buf;
287            }
288            // No recycled slab — allocate a fresh one at this size class.
289            let cap = self.size_classes[idx];
290            let mut buf = Vec::with_capacity(cap);
291            buf.resize(cap, T::default());
292            buf
293        } else {
294            // Requested size exceeds all size classes — allocate exactly.
295            let mut buf = Vec::with_capacity(min_size);
296            buf.resize(min_size, T::default());
297            buf
298        }
299    }
300
301    /// Return a buffer to the pool for reuse.
302    ///
303    /// If the buffer's capacity doesn't match any size class it is simply
304    /// dropped.
305    pub fn release(&self, buf: Vec<T>) {
306        let cap = buf.capacity();
307        let class_idx = self.size_classes.iter().position(|&s| s == cap);
308
309        if let Some(idx) = class_idx {
310            self.slabs.borrow_mut()[idx].push(buf);
311        }
312        // If it doesn't match a size class, just drop it.
313    }
314
315    /// Number of available (recycled) slabs across all size classes.
316    pub fn available_count(&self) -> usize {
317        self.slabs.borrow().iter().map(std::vec::Vec::len).sum()
318    }
319
320    /// Find the index of the smallest size class >= `min_size`.
321    fn size_class_index(&self, min_size: usize) -> Option<usize> {
322        self.size_classes.iter().position(|&s| s >= min_size)
323    }
324}
325
326impl<T: Copy + Default> Default for SlabPool<T> {
327    fn default() -> Self {
328        Self::new()
329    }
330}
331
332// ---------------------------------------------------------------------------
333// Tests
334// ---------------------------------------------------------------------------
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339
340    #[test]
341    fn test_arena_basic() {
342        let arena = Arena::new(256);
343        let mut buf = arena.alloc::<f64>(10).expect("allocation should succeed");
344        assert_eq!(buf.len(), 10);
345
346        // Write and read back.
347        for i in 0..10 {
348            buf[i] = i as f64;
349        }
350        for i in 0..10 {
351            assert!((buf[i] - i as f64).abs() < 1e-15);
352        }
353    }
354
355    #[test]
356    fn test_arena_multiple_allocs() {
357        let arena = Arena::new(512);
358        let a = arena.alloc::<f64>(100).expect("first alloc");
359        let b = arena.alloc::<f64>(100).expect("second alloc");
360        let c = arena.alloc::<f64>(100).expect("third alloc");
361
362        assert_eq!(a.len(), 100);
363        assert_eq!(b.len(), 100);
364        assert_eq!(c.len(), 100);
365
366        // All should be zero-initialized.
367        for &v in a.as_slice() {
368            assert!((v - 0.0_f64).abs() < f64::EPSILON);
369        }
370    }
371
372    #[test]
373    fn test_arena_overflow() {
374        let arena = Arena::new(10); // 10 f64 = 80 bytes
375        let ok = arena.alloc::<f64>(10);
376        assert!(ok.is_some());
377
378        // Arena is full — next allocation should fail.
379        let fail = arena.alloc::<f64>(1);
380        assert!(fail.is_none());
381    }
382
383    #[test]
384    fn test_arena_reset() {
385        let arena = Arena::new(64);
386
387        let _ = arena.alloc::<f64>(60).expect("alloc before reset");
388        assert!(arena.alloc::<f64>(10).is_none(), "should be full");
389
390        arena.reset();
391        assert_eq!(arena.used_bytes(), 0);
392
393        let buf = arena.alloc::<f64>(60).expect("alloc after reset");
394        assert_eq!(buf.len(), 60);
395    }
396
397    #[test]
398    fn test_slab_pool_acquire_release() {
399        let pool: SlabPool<f64> = SlabPool::new();
400
401        let mut buf = pool.acquire(100);
402        assert!(buf.capacity() >= 100);
403        buf[0] = 99.0;
404
405        let cap = buf.capacity();
406        pool.release(buf);
407        assert_eq!(pool.available_count(), 1);
408
409        // Re-acquire should reuse the released buffer.
410        let buf2 = pool.acquire(100);
411        assert_eq!(buf2.capacity(), cap);
412        // Should be cleared.
413        assert!((buf2[0] - 0.0_f64).abs() < f64::EPSILON);
414    }
415
416    #[test]
417    fn test_slab_pool_size_class_selection() {
418        let pool: SlabPool<f64> = SlabPool::new();
419
420        // Requesting 100 elements should yield a slab from the 256 size class.
421        let buf = pool.acquire(100);
422        assert_eq!(buf.len(), 256);
423        assert!(buf.capacity() >= 256);
424
425        // Requesting 64 should yield exactly 64.
426        let buf2 = pool.acquire(64);
427        assert_eq!(buf2.len(), 64);
428
429        // Requesting more than max size class gets exact allocation.
430        let buf3 = pool.acquire(100_000);
431        assert_eq!(buf3.len(), 100_000);
432    }
433
434    #[test]
435    fn test_arena_alloc_copy() {
436        let arena = Arena::new(256);
437        let src = [1.0_f64, 2.0, 3.0, 4.0, 5.0];
438        let buf = arena.alloc_copy(&src).expect("alloc_copy");
439        assert_eq!(buf.len(), 5);
440        assert_eq!(buf.as_slice(), &src);
441    }
442
443    #[test]
444    fn test_arena_alignment() {
445        let arena = Arena::with_byte_capacity(256);
446
447        // Allocate a u8, then a f64 — the f64 must be properly aligned.
448        let _ = arena.alloc::<u8>(1).unwrap();
449        let f = arena.alloc::<f64>(1).unwrap();
450        let ptr = f.as_slice().as_ptr() as usize;
451        assert_eq!(
452            ptr % mem::align_of::<f64>(),
453            0,
454            "f64 must be 8-byte aligned"
455        );
456    }
457}