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
135 .checked_add(size)
136 .expect("arena offset overflow");
137
138 // SAFETY: We're accessing the buffer to check its length
139 let buffer = unsafe { &*self.buffer.get() };
140 assert!(
141 end_offset <= buffer.len(),
142 "arena overflow: need {} bytes, have {} remaining",
143 size,
144 buffer.len() - aligned_offset
145 );
146
147 // Bump pointer
148 self.offset.set(end_offset);
149
150 // Write value and return reference
151 // SAFETY: We've verified:
152 // - Buffer has enough space (checked above)
153 // - Pointer is properly aligned (aligned_offset)
154 // - Lifetime is tied to arena via borrow checker
155 // - UnsafeCell allows interior mutability
156 unsafe {
157 let ptr = buffer.as_ptr().add(aligned_offset) as *mut T;
158 ptr::write(ptr, value);
159 &*ptr
160 }
161 }
162
163 /// Allocate a slice in the arena
164 ///
165 /// Returns a mutable slice of `MaybeUninit<T>` with the specified length.
166 /// Caller must initialize elements before assuming them to be initialized.
167 ///
168 /// Using `MaybeUninit` ensures memory safety - uninitialized memory cannot
169 /// be accidentally read or dropped, preventing undefined behavior.
170 ///
171 /// # Performance
172 ///
173 /// Allocation overhead: ~1ns + negligible bounds check
174 ///
175 /// # Panics
176 ///
177 /// Panics if the arena is out of space.
178 ///
179 /// # Example
180 ///
181 /// ```rust
182 /// use vibesql_executor::memory::QueryArena;
183 /// use std::mem::MaybeUninit;
184 ///
185 /// let arena = QueryArena::new();
186 /// let slice = arena.alloc_slice::<i32>(100);
187 ///
188 /// // Initialize the slice
189 /// for i in 0..100 {
190 /// slice[i] = MaybeUninit::new(i as i32);
191 /// }
192 ///
193 /// // SAFETY: All elements have been initialized above
194 /// let initialized_slice = unsafe {
195 /// std::slice::from_raw_parts(
196 /// slice.as_ptr() as *const i32,
197 /// slice.len()
198 /// )
199 /// };
200 /// assert_eq!(initialized_slice[50], 50);
201 /// ```
202 #[inline(always)]
203 #[allow(clippy::mut_from_ref)]
204 pub fn alloc_slice<T>(&self, len: usize) -> &mut [MaybeUninit<T>] {
205 if len == 0 {
206 // Return empty slice without allocating
207 return &mut [];
208 }
209
210 let offset = self.offset.get();
211 let size = mem::size_of::<T>()
212 .checked_mul(len)
213 .expect("slice size overflow");
214 let align = mem::align_of::<T>();
215
216 // Align pointer to T's alignment requirement
217 let aligned_offset = (offset + align - 1) & !(align - 1);
218
219 // Check capacity
220 let end_offset = aligned_offset
221 .checked_add(size)
222 .expect("arena offset overflow");
223
224 // SAFETY: We're accessing the buffer to check its length
225 let buffer = unsafe { &*self.buffer.get() };
226 assert!(
227 end_offset <= buffer.len(),
228 "arena overflow: need {} bytes, have {} remaining",
229 size,
230 buffer.len() - aligned_offset
231 );
232
233 // Bump pointer
234 self.offset.set(end_offset);
235
236 // Return slice of MaybeUninit<T> (safe for uninitialized memory)
237 // SAFETY: We've verified:
238 // - Buffer has enough space (checked above)
239 // - Pointer is properly aligned (aligned_offset)
240 // - Size doesn't overflow (checked above)
241 // - Lifetime is tied to arena via borrow checker
242 // - MaybeUninit<T> is safe to construct from uninitialized memory
243 // - UnsafeCell allows interior mutability
244 unsafe {
245 let ptr = buffer.as_ptr().add(aligned_offset) as *mut MaybeUninit<T>;
246 std::slice::from_raw_parts_mut(ptr, len)
247 }
248 }
249
250 /// Try to allocate a value in the arena
251 ///
252 /// Returns None if the arena is out of space.
253 ///
254 /// # Example
255 ///
256 /// ```rust
257 /// use vibesql_executor::memory::QueryArena;
258 ///
259 /// let arena = QueryArena::with_capacity(64);
260 ///
261 /// // This succeeds
262 /// assert!(arena.try_alloc(42i64).is_some());
263 ///
264 /// // Eventually this fails (arena full)
265 /// let mut count = 0;
266 /// while arena.try_alloc(count).is_some() {
267 /// count += 1;
268 /// }
269 /// ```
270 #[inline]
271 pub fn try_alloc<T>(&self, value: T) -> Option<&T> {
272 let offset = self.offset.get();
273 let size = mem::size_of::<T>();
274 let align = mem::align_of::<T>();
275
276 let aligned_offset = (offset + align - 1) & !(align - 1);
277 let end_offset = aligned_offset.checked_add(size)?;
278
279 // SAFETY: We're accessing the buffer to check its length
280 let buffer = unsafe { &*self.buffer.get() };
281
282 if end_offset > buffer.len() {
283 return None;
284 }
285
286 self.offset.set(end_offset);
287
288 // SAFETY: Same guarantees as alloc(), but checked for space
289 // - UnsafeCell allows interior mutability
290 unsafe {
291 let ptr = buffer.as_ptr().add(aligned_offset) as *mut T;
292 ptr::write(ptr, value);
293 Some(&*ptr)
294 }
295 }
296
297 /// Reset the arena, allowing it to be reused
298 ///
299 /// This doesn't deallocate the buffer, just resets the bump pointer.
300 /// All previously allocated values are invalidated.
301 ///
302 /// # Example
303 ///
304 /// ```rust
305 /// use vibesql_executor::memory::QueryArena;
306 ///
307 /// let mut arena = QueryArena::new();
308 ///
309 /// // Allocate some values (i64 = 8 bytes)
310 /// let x = arena.alloc(42i64);
311 /// assert_eq!(arena.used_bytes(), 8);
312 ///
313 /// // Reset and reuse
314 /// arena.reset();
315 /// assert_eq!(arena.used_bytes(), 0);
316 ///
317 /// // Can allocate again
318 /// let y = arena.alloc(100i64);
319 /// ```
320 pub fn reset(&mut self) {
321 self.offset.set(0);
322 // Note: We don't need to clear the buffer since we'll overwrite it
323 }
324
325 /// Get the number of bytes used in this arena
326 ///
327 /// # Example
328 ///
329 /// ```rust
330 /// use vibesql_executor::memory::QueryArena;
331 ///
332 /// let arena = QueryArena::new();
333 ///
334 /// arena.alloc(42i64); // 8 bytes
335 /// arena.alloc(3.5f64); // 8 bytes
336 ///
337 /// assert_eq!(arena.used_bytes(), 16);
338 /// ```
339 #[inline]
340 pub fn used_bytes(&self) -> usize {
341 self.offset.get()
342 }
343
344 /// Get the total capacity of this arena in bytes
345 ///
346 /// # Example
347 ///
348 /// ```rust
349 /// use vibesql_executor::memory::QueryArena;
350 ///
351 /// let arena = QueryArena::with_capacity(1024);
352 /// assert_eq!(arena.capacity_bytes(), 1024);
353 /// ```
354 #[inline]
355 pub fn capacity_bytes(&self) -> usize {
356 // SAFETY: We're only reading the length, which is safe
357 unsafe { (*self.buffer.get()).len() }
358 }
359
360 /// Get the number of bytes remaining in this arena
361 ///
362 /// # Example
363 ///
364 /// ```rust
365 /// use vibesql_executor::memory::QueryArena;
366 ///
367 /// let arena = QueryArena::with_capacity(1024);
368 /// arena.alloc(42i64); // 8 bytes
369 ///
370 /// assert_eq!(arena.remaining_bytes(), 1024 - 8);
371 /// ```
372 #[inline]
373 pub fn remaining_bytes(&self) -> usize {
374 // SAFETY: We're only reading the length, which is safe
375 unsafe { (*self.buffer.get()).len() - self.offset.get() }
376 }
377}
378
379impl Default for QueryArena {
380 fn default() -> Self {
381 Self::new()
382 }
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388
389 #[test]
390 fn test_arena_basic_allocation() {
391 let arena = QueryArena::with_capacity(1024);
392
393 let x = arena.alloc(42i64);
394 let y = arena.alloc(3.5f64);
395
396 assert_eq!(*x, 42);
397 assert_eq!(*y, 3.5);
398 }
399
400 #[test]
401 fn test_arena_slice_allocation() {
402 let arena = QueryArena::with_capacity(4096);
403
404 let slice = arena.alloc_slice::<i32>(100);
405
406 // Initialize the slice with MaybeUninit
407 for (i, elem) in slice.iter_mut().enumerate() {
408 *elem = MaybeUninit::new(i as i32);
409 }
410
411 // SAFETY: All elements have been initialized above
412 let initialized_slice = unsafe {
413 std::slice::from_raw_parts(
414 slice.as_ptr() as *const i32,
415 slice.len()
416 )
417 };
418
419 assert_eq!(initialized_slice[50], 50);
420 assert_eq!(slice.len(), 100);
421 }
422
423 #[test]
424 fn test_arena_empty_slice() {
425 let arena = QueryArena::new();
426 let slice: &mut [MaybeUninit<i32>] = arena.alloc_slice::<i32>(0);
427 assert_eq!(slice.len(), 0);
428 }
429
430 #[test]
431 fn test_arena_reset() {
432 let mut arena = QueryArena::with_capacity(1024);
433
434 let x = arena.alloc(42i64);
435 assert_eq!(*x, 42);
436 assert_eq!(arena.used_bytes(), 8);
437
438 arena.reset();
439 assert_eq!(arena.used_bytes(), 0);
440
441 // Can allocate again after reset
442 let y = arena.alloc(100i64);
443 assert_eq!(*y, 100);
444 }
445
446 #[test]
447 fn test_arena_alignment() {
448 let arena = QueryArena::with_capacity(1024);
449
450 // Allocate misaligned type first
451 let _byte = arena.alloc(1u8);
452
453 // This should be properly aligned despite previous allocation
454 let x = arena.alloc(42i64);
455 assert_eq!(*x, 42);
456
457 // Verify alignment
458 let ptr = x as *const i64 as usize;
459 assert_eq!(ptr % std::mem::align_of::<i64>(), 0);
460 }
461
462 #[test]
463 #[should_panic(expected = "arena overflow")]
464 fn test_arena_overflow() {
465 let arena = QueryArena::with_capacity(64);
466
467 // Allocate more than capacity
468 for i in 0..1000 {
469 arena.alloc(i);
470 }
471 }
472
473 #[test]
474 fn test_try_alloc_success() {
475 let arena = QueryArena::with_capacity(1024);
476
477 let x = arena.try_alloc(42i64);
478 assert!(x.is_some());
479 assert_eq!(*x.unwrap(), 42);
480 }
481
482 #[test]
483 fn test_try_alloc_failure() {
484 let arena = QueryArena::with_capacity(8);
485
486 // First allocation succeeds
487 assert!(arena.try_alloc(42i64).is_some());
488
489 // Second allocation fails (not enough space)
490 assert!(arena.try_alloc(100i64).is_none());
491 }
492
493 #[test]
494 fn test_arena_used_bytes() {
495 let arena = QueryArena::new();
496
497 assert_eq!(arena.used_bytes(), 0);
498
499 arena.alloc(42i64); // 8 bytes
500 assert_eq!(arena.used_bytes(), 8);
501
502 arena.alloc(3.5f64); // 8 bytes
503 assert_eq!(arena.used_bytes(), 16);
504 }
505
506 #[test]
507 fn test_arena_capacity() {
508 let arena = QueryArena::with_capacity(2048);
509 assert_eq!(arena.capacity_bytes(), 2048);
510 assert_eq!(arena.remaining_bytes(), 2048);
511
512 arena.alloc(42i64);
513 assert_eq!(arena.remaining_bytes(), 2048 - 8);
514 }
515
516 #[test]
517 fn test_default_arena() {
518 let arena = QueryArena::default();
519 assert_eq!(arena.capacity_bytes(), QueryArena::DEFAULT_CAPACITY);
520 }
521
522 #[test]
523 fn test_multiple_types() {
524 let arena = QueryArena::new();
525
526 let a = arena.alloc(1u8);
527 let b = arena.alloc(2u16);
528 let c = arena.alloc(3u32);
529 let d = arena.alloc(4u64);
530 let e = arena.alloc(5.0f32);
531 let f = arena.alloc(6.0f64);
532
533 assert_eq!(*a, 1);
534 assert_eq!(*b, 2);
535 assert_eq!(*c, 3);
536 assert_eq!(*d, 4);
537 assert_eq!(*e, 5.0);
538 assert_eq!(*f, 6.0);
539 }
540}