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