Skip to main content

ruvector_core/
arena.rs

1//! Arena allocator for batch operations
2//!
3//! This module provides arena-based memory allocation to reduce allocation
4//! overhead in hot paths and improve memory locality.
5//!
6//! ## Features (ADR-001)
7//!
8//! - **Cache-aligned allocations**: All allocations are aligned to cache line boundaries (64 bytes)
9//! - **Bump allocation**: O(1) allocation with minimal overhead
10//! - **Batch deallocation**: Free all allocations at once via `reset()`
11//! - **Thread-local arenas**: Per-thread allocation without synchronization
12
13use std::alloc::{alloc, dealloc, Layout};
14use std::cell::RefCell;
15use std::ptr;
16
17/// Cache line size (typically 64 bytes on modern CPUs)
18pub const CACHE_LINE_SIZE: usize = 64;
19
20/// Arena allocator for temporary allocations
21///
22/// Use this for batch operations where many temporary allocations
23/// are needed and can be freed all at once.
24pub struct Arena {
25    chunks: RefCell<Vec<Chunk>>,
26    chunk_size: usize,
27}
28
29struct Chunk {
30    data: *mut u8,
31    capacity: usize,
32    used: usize,
33}
34
35impl Arena {
36    /// Create a new arena with the specified chunk size
37    pub fn new(chunk_size: usize) -> Self {
38        Self {
39            chunks: RefCell::new(Vec::new()),
40            chunk_size,
41        }
42    }
43
44    /// Create an arena with a default 1MB chunk size
45    pub fn with_default_chunk_size() -> Self {
46        Self::new(1024 * 1024) // 1MB
47    }
48
49    /// Allocate a buffer of the specified size
50    pub fn alloc_vec<T>(&self, count: usize) -> ArenaVec<T> {
51        let size = count * std::mem::size_of::<T>();
52        let align = std::mem::align_of::<T>();
53
54        let ptr = self.alloc_raw(size, align);
55
56        ArenaVec {
57            ptr: ptr as *mut T,
58            len: 0,
59            capacity: count,
60            _phantom: std::marker::PhantomData,
61        }
62    }
63
64    /// Allocate raw bytes with specified alignment
65    fn alloc_raw(&self, size: usize, align: usize) -> *mut u8 {
66        // SECURITY: Validate alignment is a power of 2 and size is reasonable
67        assert!(
68            align > 0 && align.is_power_of_two(),
69            "Alignment must be a power of 2"
70        );
71        assert!(size > 0, "Cannot allocate zero bytes");
72        assert!(size <= isize::MAX as usize, "Allocation size too large");
73
74        let mut chunks = self.chunks.borrow_mut();
75
76        // Try to allocate from the last chunk
77        if let Some(chunk) = chunks.last_mut() {
78            // Align the current position
79            let current = chunk.used;
80            let aligned = (current + align - 1) & !(align - 1);
81
82            // SECURITY: Check for overflow in alignment calculation
83            if aligned < current {
84                panic!("Alignment calculation overflow");
85            }
86
87            let needed = aligned
88                .checked_add(size)
89                .expect("Arena allocation size overflow");
90
91            if needed <= chunk.capacity {
92                chunk.used = needed;
93                return unsafe {
94                    // SECURITY: Verify pointer arithmetic doesn't overflow
95                    let ptr = chunk.data.add(aligned);
96                    debug_assert!(ptr as usize >= chunk.data as usize, "Pointer underflow");
97                    ptr
98                };
99            }
100        }
101
102        // Need a new chunk
103        let chunk_size = self.chunk_size.max(size + align);
104        let layout = Layout::from_size_align(chunk_size, 64).unwrap();
105        let data = unsafe { alloc(layout) };
106
107        let aligned = align;
108        let chunk = Chunk {
109            data,
110            capacity: chunk_size,
111            used: aligned + size,
112        };
113
114        let ptr = unsafe { data.add(aligned) };
115        chunks.push(chunk);
116
117        ptr
118    }
119
120    /// Reset the arena, allowing reuse of allocated memory
121    pub fn reset(&self) {
122        let mut chunks = self.chunks.borrow_mut();
123        for chunk in chunks.iter_mut() {
124            chunk.used = 0;
125        }
126    }
127
128    /// Get total allocated bytes
129    pub fn allocated_bytes(&self) -> usize {
130        let chunks = self.chunks.borrow();
131        chunks.iter().map(|c| c.capacity).sum()
132    }
133
134    /// Get used bytes
135    pub fn used_bytes(&self) -> usize {
136        let chunks = self.chunks.borrow();
137        chunks.iter().map(|c| c.used).sum()
138    }
139}
140
141impl Drop for Arena {
142    fn drop(&mut self) {
143        let chunks = self.chunks.borrow();
144        for chunk in chunks.iter() {
145            let layout = Layout::from_size_align(chunk.capacity, 64).unwrap();
146            unsafe {
147                dealloc(chunk.data, layout);
148            }
149        }
150    }
151}
152
153/// Vector allocated from an arena
154pub struct ArenaVec<T> {
155    ptr: *mut T,
156    len: usize,
157    capacity: usize,
158    _phantom: std::marker::PhantomData<T>,
159}
160
161impl<T> ArenaVec<T> {
162    /// Push an element (panics if capacity exceeded)
163    pub fn push(&mut self, value: T) {
164        // SECURITY: Bounds check before pointer arithmetic
165        assert!(self.len < self.capacity, "ArenaVec capacity exceeded");
166        assert!(!self.ptr.is_null(), "ArenaVec pointer is null");
167
168        unsafe {
169            // Additional safety: verify the pointer offset is within bounds
170            let offset_ptr = self.ptr.add(self.len);
171            debug_assert!(
172                offset_ptr as usize >= self.ptr as usize,
173                "Pointer arithmetic overflow"
174            );
175            ptr::write(offset_ptr, value);
176        }
177        self.len += 1;
178    }
179
180    /// Get length
181    pub fn len(&self) -> usize {
182        self.len
183    }
184
185    /// Check if empty
186    pub fn is_empty(&self) -> bool {
187        self.len == 0
188    }
189
190    /// Get capacity
191    pub fn capacity(&self) -> usize {
192        self.capacity
193    }
194
195    /// Get as slice
196    pub fn as_slice(&self) -> &[T] {
197        // SECURITY: Bounds check before creating slice
198        assert!(self.len <= self.capacity, "Length exceeds capacity");
199        assert!(!self.ptr.is_null(), "Cannot create slice from null pointer");
200
201        unsafe { std::slice::from_raw_parts(self.ptr, self.len) }
202    }
203
204    /// Get as mutable slice
205    pub fn as_mut_slice(&mut self) -> &mut [T] {
206        // SECURITY: Bounds check before creating slice
207        assert!(self.len <= self.capacity, "Length exceeds capacity");
208        assert!(!self.ptr.is_null(), "Cannot create slice from null pointer");
209
210        unsafe { std::slice::from_raw_parts_mut(self.ptr, self.len) }
211    }
212}
213
214impl<T> std::ops::Deref for ArenaVec<T> {
215    type Target = [T];
216
217    fn deref(&self) -> &[T] {
218        self.as_slice()
219    }
220}
221
222impl<T> std::ops::DerefMut for ArenaVec<T> {
223    fn deref_mut(&mut self) -> &mut [T] {
224        self.as_mut_slice()
225    }
226}
227
228/// Thread-local arena for per-thread allocations
229thread_local! {
230    static THREAD_ARENA: RefCell<Arena> = RefCell::new(Arena::with_default_chunk_size());
231}
232
233/// Get the thread-local arena
234/// Note: Commented out due to lifetime issues with RefCell::borrow() escaping closure
235/// Use THREAD_ARENA.with(|arena| { ... }) directly instead
236/*
237pub fn thread_arena() -> impl std::ops::Deref<Target = Arena> {
238    THREAD_ARENA.with(|arena| {
239        arena.borrow()
240    })
241}
242*/
243
244/// Cache-aligned vector storage for SIMD operations (ADR-001)
245///
246/// Ensures vectors are aligned to cache line boundaries (64 bytes) for
247/// optimal SIMD operations and minimal cache misses.
248#[repr(C, align(64))]
249pub struct CacheAlignedVec {
250    data: *mut f32,
251    len: usize,
252    capacity: usize,
253}
254
255impl CacheAlignedVec {
256    /// Create a new cache-aligned vector with the given capacity
257    ///
258    /// # Panics
259    ///
260    /// Panics if memory allocation fails. For fallible allocation,
261    /// use `try_with_capacity`.
262    pub fn with_capacity(capacity: usize) -> Self {
263        Self::try_with_capacity(capacity).expect("Failed to allocate cache-aligned memory")
264    }
265
266    /// Try to create a new cache-aligned vector with the given capacity
267    ///
268    /// Returns `None` if memory allocation fails.
269    pub fn try_with_capacity(capacity: usize) -> Option<Self> {
270        // Handle zero capacity case
271        if capacity == 0 {
272            return Some(Self {
273                data: std::ptr::null_mut(),
274                len: 0,
275                capacity: 0,
276            });
277        }
278
279        // Allocate cache-line aligned memory
280        let layout =
281            Layout::from_size_align(capacity * std::mem::size_of::<f32>(), CACHE_LINE_SIZE).ok()?;
282
283        let data = unsafe { alloc(layout) as *mut f32 };
284
285        // SECURITY: Check for allocation failure
286        if data.is_null() {
287            return None;
288        }
289
290        Some(Self {
291            data,
292            len: 0,
293            capacity,
294        })
295    }
296
297    /// Create from an existing slice, copying data to cache-aligned storage
298    ///
299    /// # Panics
300    ///
301    /// Panics if memory allocation fails. For fallible allocation,
302    /// use `try_from_slice`.
303    pub fn from_slice(slice: &[f32]) -> Self {
304        Self::try_from_slice(slice).expect("Failed to allocate cache-aligned memory for slice")
305    }
306
307    /// Try to create from an existing slice, copying data to cache-aligned storage
308    ///
309    /// Returns `None` if memory allocation fails.
310    pub fn try_from_slice(slice: &[f32]) -> Option<Self> {
311        let mut vec = Self::try_with_capacity(slice.len())?;
312        if !slice.is_empty() {
313            unsafe {
314                ptr::copy_nonoverlapping(slice.as_ptr(), vec.data, slice.len());
315            }
316        }
317        vec.len = slice.len();
318        Some(vec)
319    }
320
321    /// Push an element
322    ///
323    /// # Panics
324    ///
325    /// Panics if capacity is exceeded or if the vector has zero capacity.
326    pub fn push(&mut self, value: f32) {
327        assert!(
328            self.len < self.capacity,
329            "CacheAlignedVec capacity exceeded"
330        );
331        assert!(
332            !self.data.is_null(),
333            "Cannot push to zero-capacity CacheAlignedVec"
334        );
335        unsafe {
336            *self.data.add(self.len) = value;
337        }
338        self.len += 1;
339    }
340
341    /// Get length
342    #[inline]
343    pub fn len(&self) -> usize {
344        self.len
345    }
346
347    /// Check if empty
348    #[inline]
349    pub fn is_empty(&self) -> bool {
350        self.len == 0
351    }
352
353    /// Get capacity
354    #[inline]
355    pub fn capacity(&self) -> usize {
356        self.capacity
357    }
358
359    /// Get as slice
360    #[inline]
361    pub fn as_slice(&self) -> &[f32] {
362        if self.len == 0 {
363            // SAFETY: Empty slice doesn't require valid pointer
364            return &[];
365        }
366        // SAFETY: data is valid for len elements when len > 0
367        unsafe { std::slice::from_raw_parts(self.data, self.len) }
368    }
369
370    /// Get as mutable slice
371    #[inline]
372    pub fn as_mut_slice(&mut self) -> &mut [f32] {
373        if self.len == 0 {
374            // SAFETY: Empty slice doesn't require valid pointer
375            return &mut [];
376        }
377        // SAFETY: data is valid for len elements when len > 0
378        unsafe { std::slice::from_raw_parts_mut(self.data, self.len) }
379    }
380
381    /// Get raw pointer (for SIMD operations)
382    #[inline]
383    pub fn as_ptr(&self) -> *const f32 {
384        self.data
385    }
386
387    /// Get mutable raw pointer (for SIMD operations)
388    #[inline]
389    pub fn as_mut_ptr(&mut self) -> *mut f32 {
390        self.data
391    }
392
393    /// Check if properly aligned for SIMD
394    ///
395    /// Returns `true` for zero-capacity vectors (considered trivially aligned).
396    #[inline]
397    pub fn is_aligned(&self) -> bool {
398        if self.data.is_null() {
399            // Zero-capacity vectors are considered aligned
400            return self.capacity == 0;
401        }
402        (self.data as usize) % CACHE_LINE_SIZE == 0
403    }
404
405    /// Clear the vector (sets len to 0, doesn't deallocate)
406    pub fn clear(&mut self) {
407        self.len = 0;
408    }
409}
410
411impl Drop for CacheAlignedVec {
412    fn drop(&mut self) {
413        if !self.data.is_null() && self.capacity > 0 {
414            let layout = Layout::from_size_align(
415                self.capacity * std::mem::size_of::<f32>(),
416                CACHE_LINE_SIZE,
417            )
418            .expect("Invalid layout");
419
420            unsafe {
421                dealloc(self.data as *mut u8, layout);
422            }
423        }
424    }
425}
426
427impl std::ops::Deref for CacheAlignedVec {
428    type Target = [f32];
429
430    fn deref(&self) -> &[f32] {
431        self.as_slice()
432    }
433}
434
435impl std::ops::DerefMut for CacheAlignedVec {
436    fn deref_mut(&mut self) -> &mut [f32] {
437        self.as_mut_slice()
438    }
439}
440
441// Safety: The raw pointer is owned and not shared
442unsafe impl Send for CacheAlignedVec {}
443unsafe impl Sync for CacheAlignedVec {}
444
445/// Batch vector allocator for processing multiple vectors (ADR-001)
446///
447/// Allocates contiguous, cache-aligned storage for a batch of vectors,
448/// enabling efficient SIMD processing and minimal cache misses.
449pub struct BatchVectorAllocator {
450    data: *mut f32,
451    dimensions: usize,
452    capacity: usize,
453    count: usize,
454}
455
456impl BatchVectorAllocator {
457    /// Create allocator for vectors of given dimensions
458    ///
459    /// # Panics
460    ///
461    /// Panics if memory allocation fails. For fallible allocation,
462    /// use `try_new`.
463    pub fn new(dimensions: usize, initial_capacity: usize) -> Self {
464        Self::try_new(dimensions, initial_capacity)
465            .expect("Failed to allocate batch vector storage")
466    }
467
468    /// Try to create allocator for vectors of given dimensions
469    ///
470    /// Returns `None` if memory allocation fails.
471    pub fn try_new(dimensions: usize, initial_capacity: usize) -> Option<Self> {
472        // Handle zero capacity case
473        if dimensions == 0 || initial_capacity == 0 {
474            return Some(Self {
475                data: std::ptr::null_mut(),
476                dimensions,
477                capacity: initial_capacity,
478                count: 0,
479            });
480        }
481
482        let total_floats = dimensions * initial_capacity;
483
484        let layout =
485            Layout::from_size_align(total_floats * std::mem::size_of::<f32>(), CACHE_LINE_SIZE)
486                .ok()?;
487
488        let data = unsafe { alloc(layout) as *mut f32 };
489
490        // SECURITY: Check for allocation failure
491        if data.is_null() {
492            return None;
493        }
494
495        Some(Self {
496            data,
497            dimensions,
498            capacity: initial_capacity,
499            count: 0,
500        })
501    }
502
503    /// Add a vector, returns its index
504    ///
505    /// # Panics
506    ///
507    /// Panics if the allocator is full, dimensions mismatch, or allocator has zero capacity.
508    pub fn add(&mut self, vector: &[f32]) -> usize {
509        assert_eq!(vector.len(), self.dimensions, "Vector dimension mismatch");
510        assert!(self.count < self.capacity, "Batch allocator full");
511        assert!(
512            !self.data.is_null(),
513            "Cannot add to zero-capacity BatchVectorAllocator"
514        );
515
516        let offset = self.count * self.dimensions;
517        unsafe {
518            ptr::copy_nonoverlapping(vector.as_ptr(), self.data.add(offset), self.dimensions);
519        }
520
521        let index = self.count;
522        self.count += 1;
523        index
524    }
525
526    /// Get a vector by index
527    pub fn get(&self, index: usize) -> &[f32] {
528        assert!(index < self.count, "Index out of bounds");
529        let offset = index * self.dimensions;
530        unsafe { std::slice::from_raw_parts(self.data.add(offset), self.dimensions) }
531    }
532
533    /// Get mutable vector by index
534    pub fn get_mut(&mut self, index: usize) -> &mut [f32] {
535        assert!(index < self.count, "Index out of bounds");
536        let offset = index * self.dimensions;
537        unsafe { std::slice::from_raw_parts_mut(self.data.add(offset), self.dimensions) }
538    }
539
540    /// Get raw pointer to vector at index (for SIMD)
541    #[inline]
542    pub fn ptr_at(&self, index: usize) -> *const f32 {
543        assert!(index < self.count, "Index out of bounds");
544        let offset = index * self.dimensions;
545        unsafe { self.data.add(offset) }
546    }
547
548    /// Number of vectors stored
549    #[inline]
550    pub fn len(&self) -> usize {
551        self.count
552    }
553
554    /// Check if empty
555    #[inline]
556    pub fn is_empty(&self) -> bool {
557        self.count == 0
558    }
559
560    /// Dimensions per vector
561    #[inline]
562    pub fn dimensions(&self) -> usize {
563        self.dimensions
564    }
565
566    /// Reset allocator (keeps memory)
567    pub fn clear(&mut self) {
568        self.count = 0;
569    }
570}
571
572impl Drop for BatchVectorAllocator {
573    fn drop(&mut self) {
574        if !self.data.is_null() {
575            let layout = Layout::from_size_align(
576                self.dimensions * self.capacity * std::mem::size_of::<f32>(),
577                CACHE_LINE_SIZE,
578            )
579            .expect("Invalid layout");
580
581            unsafe {
582                dealloc(self.data as *mut u8, layout);
583            }
584        }
585    }
586}
587
588// Safety: The raw pointer is owned and not shared
589unsafe impl Send for BatchVectorAllocator {}
590unsafe impl Sync for BatchVectorAllocator {}
591
592#[cfg(test)]
593mod tests {
594    use super::*;
595
596    #[test]
597    fn test_arena_alloc() {
598        let arena = Arena::new(1024);
599
600        let mut vec1 = arena.alloc_vec::<f32>(10);
601        vec1.push(1.0);
602        vec1.push(2.0);
603        vec1.push(3.0);
604
605        assert_eq!(vec1.len(), 3);
606        assert_eq!(vec1[0], 1.0);
607        assert_eq!(vec1[1], 2.0);
608        assert_eq!(vec1[2], 3.0);
609    }
610
611    #[test]
612    fn test_arena_multiple_allocs() {
613        let arena = Arena::new(1024);
614
615        let vec1 = arena.alloc_vec::<u32>(100);
616        let vec2 = arena.alloc_vec::<u64>(50);
617        let vec3 = arena.alloc_vec::<f32>(200);
618
619        assert_eq!(vec1.capacity(), 100);
620        assert_eq!(vec2.capacity(), 50);
621        assert_eq!(vec3.capacity(), 200);
622    }
623
624    #[test]
625    fn test_arena_reset() {
626        let arena = Arena::new(1024);
627
628        {
629            let _vec1 = arena.alloc_vec::<f32>(100);
630            let _vec2 = arena.alloc_vec::<f32>(100);
631        }
632
633        let used_before = arena.used_bytes();
634        arena.reset();
635        let used_after = arena.used_bytes();
636
637        assert!(used_after < used_before);
638    }
639
640    #[test]
641    fn test_cache_aligned_vec() {
642        let mut vec = CacheAlignedVec::with_capacity(100);
643
644        // Check alignment
645        assert!(vec.is_aligned(), "Vector should be cache-aligned");
646
647        // Test push
648        for i in 0..50 {
649            vec.push(i as f32);
650        }
651        assert_eq!(vec.len(), 50);
652
653        // Test slice access
654        let slice = vec.as_slice();
655        assert_eq!(slice[0], 0.0);
656        assert_eq!(slice[49], 49.0);
657    }
658
659    #[test]
660    fn test_cache_aligned_vec_from_slice() {
661        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
662        let aligned = CacheAlignedVec::from_slice(&data);
663
664        assert!(aligned.is_aligned());
665        assert_eq!(aligned.len(), 5);
666        assert_eq!(aligned.as_slice(), &data[..]);
667    }
668
669    #[test]
670    fn test_batch_vector_allocator() {
671        let mut allocator = BatchVectorAllocator::new(4, 10);
672
673        let v1 = vec![1.0, 2.0, 3.0, 4.0];
674        let v2 = vec![5.0, 6.0, 7.0, 8.0];
675
676        let idx1 = allocator.add(&v1);
677        let idx2 = allocator.add(&v2);
678
679        assert_eq!(idx1, 0);
680        assert_eq!(idx2, 1);
681        assert_eq!(allocator.len(), 2);
682
683        // Test retrieval
684        assert_eq!(allocator.get(0), &v1[..]);
685        assert_eq!(allocator.get(1), &v2[..]);
686    }
687
688    #[test]
689    fn test_batch_allocator_clear() {
690        let mut allocator = BatchVectorAllocator::new(3, 5);
691
692        allocator.add(&[1.0, 2.0, 3.0]);
693        allocator.add(&[4.0, 5.0, 6.0]);
694
695        assert_eq!(allocator.len(), 2);
696
697        allocator.clear();
698        assert_eq!(allocator.len(), 0);
699
700        // Should be able to add again
701        allocator.add(&[7.0, 8.0, 9.0]);
702        assert_eq!(allocator.len(), 1);
703    }
704}