vibesql_executor/memory/
arena.rs

1//! Arena allocator for query-scoped memory allocation
2//!
3//! This module provides a bump-pointer arena allocator that eliminates
4//! malloc/free overhead by allocating from a pre-allocated buffer.
5//!
6//! # Performance
7//!
8//! - Allocation: ~1ns (vs ~100ns for malloc)
9//! - Deallocation: 0ns (arena dropped at end)
10//! - Cache locality: sequential allocations in contiguous memory
11//! - No fragmentation: single buffer reused per query
12//!
13//! # Safety
14//!
15//! All allocations are tied to the arena's lifetime via Rust's borrow checker.
16//! References returned cannot outlive the arena.
17
18use std::alloc::{Layout, alloc, dealloc};
19use std::cell::Cell;
20use std::marker::PhantomData;
21use std::mem::{self, MaybeUninit};
22use std::ptr;
23
24/// Arena allocator for query-scoped allocations
25///
26/// Uses a bump-pointer strategy to allocate from a pre-allocated buffer.
27/// All allocations are freed when the arena is reset or dropped.
28///
29/// # Example
30///
31/// ```rust
32/// use vibesql_executor::memory::QueryArena;
33/// use std::mem::MaybeUninit;
34///
35/// let arena = QueryArena::with_capacity(1024);
36///
37/// // Allocate a value
38/// let x = arena.alloc(42i64);
39/// assert_eq!(*x, 42);
40///
41/// // Allocate a slice (returns uninitialized memory)
42/// let slice = arena.alloc_slice::<i32>(10);
43/// for i in 0..10 {
44///     slice[i] = MaybeUninit::new(i as i32);
45/// }
46/// ```
47/// Maximum alignment we support (16 bytes covers most use cases including SIMD types)
48const MAX_ALIGN: usize = 16;
49
50pub struct QueryArena {
51    /// Pre-allocated buffer for all allocations (aligned to MAX_ALIGN)
52    buffer: *mut u8,
53    /// Capacity of the buffer in bytes
54    capacity: usize,
55    /// Current offset into the buffer (bump pointer)
56    offset: Cell<usize>,
57    /// Phantom data to ensure proper variance
58    _marker: PhantomData<Cell<u8>>,
59}
60
61impl QueryArena {
62    /// Default arena size: 10MB
63    ///
64    /// Based on TPC-H Q6 analysis:
65    /// - 60K rows × 16 columns = 960K SqlValues
66    /// - Intermediate results: ~2MB
67    /// - Expression temporaries: ~1MB
68    /// - Safety margin: 7MB
69    pub const DEFAULT_CAPACITY: usize = 10 * 1024 * 1024; // 10MB
70
71    /// Create a new arena with the specified capacity in bytes
72    ///
73    /// # Arguments
74    ///
75    /// * `bytes` - Number of bytes to pre-allocate
76    ///
77    /// # Example
78    ///
79    /// ```rust
80    /// use vibesql_executor::memory::QueryArena;
81    ///
82    /// // 1MB arena
83    /// let arena = QueryArena::with_capacity(1024 * 1024);
84    /// ```
85    pub fn with_capacity(bytes: usize) -> Self {
86        // SAFETY: Allocate properly aligned memory
87        let layout = Layout::from_size_align(bytes, MAX_ALIGN)
88            .expect("invalid arena layout");
89        let buffer = unsafe { alloc(layout) };
90        if buffer.is_null() {
91            panic!("arena allocation failed");
92        }
93        Self {
94            buffer,
95            capacity: bytes,
96            offset: Cell::new(0),
97            _marker: PhantomData,
98        }
99    }
100
101    /// Create a new arena with the default capacity (10MB)
102    ///
103    /// # Example
104    ///
105    /// ```rust
106    /// use vibesql_executor::memory::QueryArena;
107    ///
108    /// let arena = QueryArena::new();
109    /// ```
110    pub fn new() -> Self {
111        Self::with_capacity(Self::DEFAULT_CAPACITY)
112    }
113
114    /// Allocate a value in the arena
115    ///
116    /// Returns a reference to the allocated value with lifetime tied to the arena.
117    ///
118    /// # Performance
119    ///
120    /// Allocation overhead: ~1ns (vs ~100ns for heap allocation)
121    ///
122    /// # Panics
123    ///
124    /// Panics if the arena is out of space. Use `try_alloc` for fallible allocation.
125    ///
126    /// # Example
127    ///
128    /// ```rust
129    /// use vibesql_executor::memory::QueryArena;
130    ///
131    /// let arena = QueryArena::new();
132    /// let x = arena.alloc(42i64);
133    /// let y = arena.alloc(3.5f64);
134    ///
135    /// assert_eq!(*x, 42);
136    /// assert_eq!(*y, 3.5);
137    /// ```
138    #[inline(always)]
139    pub fn alloc<T>(&self, value: T) -> &T {
140        let offset = self.offset.get();
141        let size = mem::size_of::<T>();
142        let align = mem::align_of::<T>();
143
144        assert!(
145            align <= MAX_ALIGN,
146            "arena does not support alignment greater than {}",
147            MAX_ALIGN
148        );
149
150        // Align pointer to T's alignment requirement
151        let aligned_offset = (offset + align - 1) & !(align - 1);
152
153        // Check capacity
154        let end_offset = aligned_offset.checked_add(size).expect("arena offset overflow");
155
156        assert!(
157            end_offset <= self.capacity,
158            "arena overflow: need {} bytes, have {} remaining",
159            size,
160            self.capacity - aligned_offset
161        );
162
163        // Bump pointer
164        self.offset.set(end_offset);
165
166        // Write value and return reference
167        // SAFETY: We've verified:
168        // - Buffer has enough space (checked above)
169        // - Pointer is properly aligned (buffer is MAX_ALIGN aligned, aligned_offset ensures T's alignment)
170        // - Lifetime is tied to arena via borrow checker
171        unsafe {
172            let ptr = self.buffer.add(aligned_offset) as *mut T;
173            ptr::write(ptr, value);
174            &*ptr
175        }
176    }
177
178    /// Allocate a slice in the arena
179    ///
180    /// Returns a mutable slice of `MaybeUninit<T>` with the specified length.
181    /// Caller must initialize elements before assuming them to be initialized.
182    ///
183    /// Using `MaybeUninit` ensures memory safety - uninitialized memory cannot
184    /// be accidentally read or dropped, preventing undefined behavior.
185    ///
186    /// # Performance
187    ///
188    /// Allocation overhead: ~1ns + negligible bounds check
189    ///
190    /// # Panics
191    ///
192    /// Panics if the arena is out of space.
193    ///
194    /// # Example
195    ///
196    /// ```rust
197    /// use vibesql_executor::memory::QueryArena;
198    /// use std::mem::MaybeUninit;
199    ///
200    /// let arena = QueryArena::new();
201    /// let slice = arena.alloc_slice::<i32>(100);
202    ///
203    /// // Initialize the slice
204    /// for i in 0..100 {
205    ///     slice[i] = MaybeUninit::new(i as i32);
206    /// }
207    ///
208    /// // SAFETY: All elements have been initialized above
209    /// let initialized_slice = unsafe {
210    ///     std::slice::from_raw_parts(
211    ///         slice.as_ptr() as *const i32,
212    ///         slice.len()
213    ///     )
214    /// };
215    /// assert_eq!(initialized_slice[50], 50);
216    /// ```
217    #[inline(always)]
218    #[allow(clippy::mut_from_ref)]
219    pub fn alloc_slice<T>(&self, len: usize) -> &mut [MaybeUninit<T>] {
220        if len == 0 {
221            // Return empty slice without allocating
222            return &mut [];
223        }
224
225        let offset = self.offset.get();
226        let size = mem::size_of::<T>().checked_mul(len).expect("slice size overflow");
227        let align = mem::align_of::<T>();
228
229        assert!(
230            align <= MAX_ALIGN,
231            "arena does not support alignment greater than {}",
232            MAX_ALIGN
233        );
234
235        // Align pointer to T's alignment requirement
236        let aligned_offset = (offset + align - 1) & !(align - 1);
237
238        // Check capacity
239        let end_offset = aligned_offset.checked_add(size).expect("arena offset overflow");
240
241        assert!(
242            end_offset <= self.capacity,
243            "arena overflow: need {} bytes, have {} remaining",
244            size,
245            self.capacity - aligned_offset
246        );
247
248        // Bump pointer
249        self.offset.set(end_offset);
250
251        // Return slice of MaybeUninit<T> (safe for uninitialized memory)
252        // SAFETY: We've verified:
253        // - Buffer has enough space (checked above)
254        // - Pointer is properly aligned (buffer is MAX_ALIGN aligned, aligned_offset ensures T's alignment)
255        // - Size doesn't overflow (checked above)
256        // - Lifetime is tied to arena via borrow checker
257        // - MaybeUninit<T> is safe to construct from uninitialized memory
258        unsafe {
259            let ptr = self.buffer.add(aligned_offset) as *mut MaybeUninit<T>;
260            std::slice::from_raw_parts_mut(ptr, len)
261        }
262    }
263
264    /// Try to allocate a value in the arena
265    ///
266    /// Returns None if the arena is out of space.
267    ///
268    /// # Example
269    ///
270    /// ```rust
271    /// use vibesql_executor::memory::QueryArena;
272    ///
273    /// let arena = QueryArena::with_capacity(64);
274    ///
275    /// // This succeeds
276    /// assert!(arena.try_alloc(42i64).is_some());
277    ///
278    /// // Eventually this fails (arena full)
279    /// let mut count = 0;
280    /// while arena.try_alloc(count).is_some() {
281    ///     count += 1;
282    /// }
283    /// ```
284    #[inline]
285    pub fn try_alloc<T>(&self, value: T) -> Option<&T> {
286        let offset = self.offset.get();
287        let size = mem::size_of::<T>();
288        let align = mem::align_of::<T>();
289
290        if align > MAX_ALIGN {
291            return None;
292        }
293
294        let aligned_offset = (offset + align - 1) & !(align - 1);
295        let end_offset = aligned_offset.checked_add(size)?;
296
297        if end_offset > self.capacity {
298            return None;
299        }
300
301        self.offset.set(end_offset);
302
303        // SAFETY: Same guarantees as alloc(), but checked for space
304        unsafe {
305            let ptr = self.buffer.add(aligned_offset) as *mut T;
306            ptr::write(ptr, value);
307            Some(&*ptr)
308        }
309    }
310
311    /// Reset the arena, allowing it to be reused
312    ///
313    /// This doesn't deallocate the buffer, just resets the bump pointer.
314    /// All previously allocated values are invalidated.
315    ///
316    /// # Example
317    ///
318    /// ```rust
319    /// use vibesql_executor::memory::QueryArena;
320    ///
321    /// let mut arena = QueryArena::new();
322    ///
323    /// // Allocate some values (i64 = 8 bytes)
324    /// let x = arena.alloc(42i64);
325    /// assert_eq!(arena.used_bytes(), 8);
326    ///
327    /// // Reset and reuse
328    /// arena.reset();
329    /// assert_eq!(arena.used_bytes(), 0);
330    ///
331    /// // Can allocate again
332    /// let y = arena.alloc(100i64);
333    /// ```
334    pub fn reset(&mut self) {
335        self.offset.set(0);
336        // Note: We don't need to clear the buffer since we'll overwrite it
337    }
338
339    /// Get the number of bytes used in this arena
340    ///
341    /// # Example
342    ///
343    /// ```rust
344    /// use vibesql_executor::memory::QueryArena;
345    ///
346    /// let arena = QueryArena::new();
347    ///
348    /// arena.alloc(42i64); // 8 bytes
349    /// arena.alloc(3.5f64); // 8 bytes
350    ///
351    /// assert_eq!(arena.used_bytes(), 16);
352    /// ```
353    #[inline]
354    pub fn used_bytes(&self) -> usize {
355        self.offset.get()
356    }
357
358    /// Get the total capacity of this arena in bytes
359    ///
360    /// # Example
361    ///
362    /// ```rust
363    /// use vibesql_executor::memory::QueryArena;
364    ///
365    /// let arena = QueryArena::with_capacity(1024);
366    /// assert_eq!(arena.capacity_bytes(), 1024);
367    /// ```
368    #[inline]
369    pub fn capacity_bytes(&self) -> usize {
370        self.capacity
371    }
372
373    /// Get the number of bytes remaining in this arena
374    ///
375    /// # Example
376    ///
377    /// ```rust
378    /// use vibesql_executor::memory::QueryArena;
379    ///
380    /// let arena = QueryArena::with_capacity(1024);
381    /// arena.alloc(42i64); // 8 bytes
382    ///
383    /// assert_eq!(arena.remaining_bytes(), 1024 - 8);
384    /// ```
385    #[inline]
386    pub fn remaining_bytes(&self) -> usize {
387        self.capacity - self.offset.get()
388    }
389}
390
391impl Drop for QueryArena {
392    fn drop(&mut self) {
393        // SAFETY: buffer was allocated with Layout::from_size_align(capacity, MAX_ALIGN)
394        unsafe {
395            let layout = Layout::from_size_align_unchecked(self.capacity, MAX_ALIGN);
396            dealloc(self.buffer, layout);
397        }
398    }
399}
400
401// SAFETY: QueryArena can be sent between threads because:
402// - The buffer is only accessed through methods that take &self
403// - Cell provides interior mutability but is single-threaded
404// - The arena is not Sync (cannot be shared between threads) but is Send
405unsafe impl Send for QueryArena {}
406
407impl Default for QueryArena {
408    fn default() -> Self {
409        Self::new()
410    }
411}
412
413#[cfg(test)]
414mod tests {
415    use super::*;
416
417    #[test]
418    fn test_arena_basic_allocation() {
419        let arena = QueryArena::with_capacity(1024);
420
421        let x = arena.alloc(42i64);
422        let y = arena.alloc(3.5f64);
423
424        assert_eq!(*x, 42);
425        assert_eq!(*y, 3.5);
426    }
427
428    #[test]
429    fn test_arena_slice_allocation() {
430        let arena = QueryArena::with_capacity(4096);
431
432        let slice = arena.alloc_slice::<i32>(100);
433
434        // Initialize the slice with MaybeUninit
435        for (i, elem) in slice.iter_mut().enumerate() {
436            *elem = MaybeUninit::new(i as i32);
437        }
438
439        // SAFETY: All elements have been initialized above
440        let initialized_slice =
441            unsafe { std::slice::from_raw_parts(slice.as_ptr() as *const i32, slice.len()) };
442
443        assert_eq!(initialized_slice[50], 50);
444        assert_eq!(slice.len(), 100);
445    }
446
447    #[test]
448    fn test_arena_empty_slice() {
449        let arena = QueryArena::new();
450        let slice: &mut [MaybeUninit<i32>] = arena.alloc_slice::<i32>(0);
451        assert_eq!(slice.len(), 0);
452    }
453
454    #[test]
455    fn test_arena_reset() {
456        let mut arena = QueryArena::with_capacity(1024);
457
458        let x = arena.alloc(42i64);
459        assert_eq!(*x, 42);
460        assert_eq!(arena.used_bytes(), 8);
461
462        arena.reset();
463        assert_eq!(arena.used_bytes(), 0);
464
465        // Can allocate again after reset
466        let y = arena.alloc(100i64);
467        assert_eq!(*y, 100);
468    }
469
470    #[test]
471    fn test_arena_alignment() {
472        let arena = QueryArena::with_capacity(1024);
473
474        // Allocate misaligned type first
475        let _byte = arena.alloc(1u8);
476
477        // This should be properly aligned despite previous allocation
478        let x = arena.alloc(42i64);
479        assert_eq!(*x, 42);
480
481        // Verify alignment
482        let ptr = x as *const i64 as usize;
483        assert_eq!(ptr % std::mem::align_of::<i64>(), 0);
484    }
485
486    #[test]
487    #[should_panic(expected = "arena overflow")]
488    fn test_arena_overflow() {
489        let arena = QueryArena::with_capacity(64);
490
491        // Allocate more than capacity
492        for i in 0..1000 {
493            arena.alloc(i);
494        }
495    }
496
497    #[test]
498    fn test_try_alloc_success() {
499        let arena = QueryArena::with_capacity(1024);
500
501        let x = arena.try_alloc(42i64);
502        assert!(x.is_some());
503        assert_eq!(*x.unwrap(), 42);
504    }
505
506    #[test]
507    fn test_try_alloc_failure() {
508        let arena = QueryArena::with_capacity(8);
509
510        // First allocation succeeds
511        assert!(arena.try_alloc(42i64).is_some());
512
513        // Second allocation fails (not enough space)
514        assert!(arena.try_alloc(100i64).is_none());
515    }
516
517    #[test]
518    fn test_arena_used_bytes() {
519        let arena = QueryArena::new();
520
521        assert_eq!(arena.used_bytes(), 0);
522
523        arena.alloc(42i64); // 8 bytes
524        assert_eq!(arena.used_bytes(), 8);
525
526        arena.alloc(3.5f64); // 8 bytes
527        assert_eq!(arena.used_bytes(), 16);
528    }
529
530    #[test]
531    fn test_arena_capacity() {
532        let arena = QueryArena::with_capacity(2048);
533        assert_eq!(arena.capacity_bytes(), 2048);
534        assert_eq!(arena.remaining_bytes(), 2048);
535
536        arena.alloc(42i64);
537        assert_eq!(arena.remaining_bytes(), 2048 - 8);
538    }
539
540    #[test]
541    fn test_default_arena() {
542        let arena = QueryArena::default();
543        assert_eq!(arena.capacity_bytes(), QueryArena::DEFAULT_CAPACITY);
544    }
545
546    #[test]
547    fn test_multiple_types() {
548        let arena = QueryArena::new();
549
550        let a = arena.alloc(1u8);
551        let b = arena.alloc(2u16);
552        let c = arena.alloc(3u32);
553        let d = arena.alloc(4u64);
554        let e = arena.alloc(5.0f32);
555        let f = arena.alloc(6.0f64);
556
557        assert_eq!(*a, 1);
558        assert_eq!(*b, 2);
559        assert_eq!(*c, 3);
560        assert_eq!(*d, 4);
561        assert_eq!(*e, 5.0);
562        assert_eq!(*f, 6.0);
563    }
564}