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}