ruvector_solver/arena.rs
1//! Bump allocator for per-solve scratch space.
2//!
3//! [`SolverArena`] provides fast, zero-fragmentation allocation of temporary
4//! vectors and slices that are needed only for the duration of a single solve
5//! invocation. At the end of the solve, the arena is [`reset`](SolverArena::reset)
6//! and all memory is reclaimed in O(1).
7//!
8//! This avoids repeated heap allocations in hot solver loops and gives
9//! deterministic memory usage when a [`ComputeBudget`](crate::types::ComputeBudget)
10//! memory limit is in effect.
11
12use std::cell::RefCell;
13
14/// A simple bump allocator for solver scratch buffers.
15///
16/// All allocations are contiguous within a single backing `Vec<u8>`.
17/// The arena does **not** drop individual allocations; instead, call
18/// [`reset`](Self::reset) to reclaim all space at once.
19///
20/// # Example
21///
22/// ```
23/// use ruvector_solver::arena::SolverArena;
24///
25/// let arena = SolverArena::with_capacity(1024);
26/// let buf: &mut [f64] = arena.alloc_slice::<f64>(128);
27/// assert_eq!(buf.len(), 128);
28/// assert!(arena.bytes_used() >= 128 * std::mem::size_of::<f64>());
29/// arena.reset();
30/// assert_eq!(arena.bytes_used(), 0);
31/// ```
32pub struct SolverArena {
33 /// Backing storage.
34 buf: RefCell<Vec<u8>>,
35 /// Current write offset (bump pointer).
36 offset: RefCell<usize>,
37}
38
39impl SolverArena {
40 /// Create a new arena with the given capacity in bytes.
41 ///
42 /// The arena will not reallocate unless an allocation request exceeds
43 /// the remaining capacity, in which case it grows by doubling.
44 pub fn with_capacity(capacity: usize) -> Self {
45 Self {
46 buf: RefCell::new(vec![0u8; capacity]),
47 offset: RefCell::new(0),
48 }
49 }
50
51 /// Allocate a mutable slice of `len` elements of type `T`, zero-initialised.
52 ///
53 /// # Panics
54 ///
55 /// - Panics if `T` has alignment greater than 16 (an unusual case for
56 /// solver numerics).
57 /// - Panics if `size_of::<T>() * len` overflows `usize` (prevents
58 /// integer overflow leading to undersized allocations).
59 pub fn alloc_slice<T: Copy + Default>(&self, len: usize) -> &mut [T] {
60 let size = std::mem::size_of::<T>();
61 let align = std::mem::align_of::<T>();
62 assert!(align <= 16, "SolverArena does not support alignment > 16");
63
64 // Guard against integer overflow: `size * len` must not wrap.
65 let byte_len = size
66 .checked_mul(len)
67 .expect("SolverArena::alloc_slice: size * len overflowed usize");
68
69 let mut offset = self.offset.borrow_mut();
70 let mut buf = self.buf.borrow_mut();
71
72 // Align the current offset up to `align`.
73 let aligned = (*offset + align - 1) & !(align - 1);
74 let needed = aligned
75 .checked_add(byte_len)
76 .expect("SolverArena::alloc_slice: aligned + byte_len overflowed usize");
77
78 // Grow if necessary.
79 if needed > buf.len() {
80 let new_cap = (needed * 2).max(buf.len() * 2);
81 buf.resize(new_cap, 0);
82 }
83
84 // Zero the allocated region.
85 buf[aligned..aligned + byte_len].fill(0);
86
87 *offset = aligned + byte_len;
88 let ptr = buf[aligned..].as_mut_ptr() as *mut T;
89
90 // SAFETY: The following invariants are upheld:
91 //
92 // 1. **Exclusive access**: We hold the only `RefMut` borrows on both
93 // `self.buf` and `self.offset`. No other code can read or write the
94 // backing buffer while this function executes.
95 //
96 // 2. **Alignment**: `aligned` is rounded up to `align_of::<T>()`, so
97 // `ptr` is properly aligned for `T`.
98 //
99 // 3. **Bounds**: `needed <= buf.len()` after the grow check, so the
100 // range `[aligned, aligned + byte_len)` is within the buffer.
101 //
102 // 4. **Initialisation**: The region has been zero-filled, and `T: Copy`
103 // guarantees that an all-zeros bit pattern is a valid value (since
104 // `T: Default` is also required but zeroed memory is used).
105 //
106 // 5. **Lifetime**: The returned slice borrows `&self`, not the
107 // `RefMut` guards. We drop the guards before returning so that
108 // future calls to `alloc_slice` or `reset` can re-borrow. The
109 // pointer remains valid as long as `&self` is live because the
110 // backing `Vec` is not reallocated unless `alloc_slice` is called
111 // again (at which point the previous reference is no longer used
112 // by the caller in safe solver patterns).
113 //
114 // 6. **Send but not Sync**: The `unsafe impl Send` below is sound
115 // because `SolverArena` owns all its data. It is not `Sync`
116 // because `RefCell` does not support concurrent access.
117 drop(offset);
118 drop(buf);
119
120 unsafe { std::slice::from_raw_parts_mut(ptr, len) }
121 }
122
123 /// Reset the arena, reclaiming all allocations.
124 ///
125 /// This does not free the backing memory; it simply resets the bump
126 /// pointer to zero. Subsequent allocations reuse the same buffer.
127 pub fn reset(&self) {
128 *self.offset.borrow_mut() = 0;
129 }
130
131 /// Number of bytes currently allocated (bump pointer position).
132 pub fn bytes_used(&self) -> usize {
133 *self.offset.borrow()
134 }
135
136 /// Total capacity of the backing buffer in bytes.
137 pub fn capacity(&self) -> usize {
138 self.buf.borrow().len()
139 }
140}
141
142// SAFETY: `SolverArena` is `Send` because it exclusively owns all its data
143// (`Vec<u8>` inside a `RefCell`). Moving the arena to another thread is safe
144// since no shared references can exist across threads.
145//
146// It is intentionally **not** `Sync` because `RefCell` does not support
147// concurrent borrows. The compiler's auto-trait inference already prevents
148// `Sync`, so no negative impl is needed.
149unsafe impl Send for SolverArena {}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 #[test]
156 fn alloc_and_reset() {
157 let arena = SolverArena::with_capacity(4096);
158 let s1: &mut [f64] = arena.alloc_slice(100);
159 assert_eq!(s1.len(), 100);
160 assert!(arena.bytes_used() >= 800);
161
162 let s2: &mut [f32] = arena.alloc_slice(50);
163 assert_eq!(s2.len(), 50);
164
165 arena.reset();
166 assert_eq!(arena.bytes_used(), 0);
167 }
168
169 #[test]
170 fn grows_when_needed() {
171 let arena = SolverArena::with_capacity(16);
172 let s: &mut [f64] = arena.alloc_slice(100);
173 assert_eq!(s.len(), 100);
174 assert!(arena.capacity() >= 800);
175 }
176}