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}