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