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}