Skip to main content

cjc_runtime/
binned_alloc.rs

1//! Deterministic Binned Free Lists
2//!
3//! Provides a size-class allocator with 13 bins (16 B – 64 KB).
4//! Each bin maintains a LIFO free list for O(1) alloc/free.
5//!
6//! # Determinism guarantees
7//!
8//! - **LIFO push/pop**: same free+alloc sequence → same addresses.
9//! - **No OS return**: pages are never returned to the OS during normal
10//!   execution. Memory grows monotonically, freed memory is recycled.
11//! - **Deterministic iteration**: bins are indexed by size class, not
12//!   hash-based — iteration order is always the same.
13//!
14//! # Size classes
15//!
16//! | Bin | Size    |
17//! |-----|---------|
18//! |   0 |    16 B |
19//! |   1 |    32 B |
20//! |   2 |    48 B |
21//! |   3 |    64 B |
22//! |   4 |   128 B |
23//! |   5 |   256 B |
24//! |   6 |   512 B |
25//! |   7 |  1 KB   |
26//! |   8 |  2 KB   |
27//! |   9 |  4 KB   |
28//! |  10 |  8 KB   |
29//! |  11 | 16 KB   |
30//! |  12 | 64 KB   |
31//!
32//! Allocations larger than 64 KB go to a dedicated overflow list.
33
34/// Number of size-class bins.
35const NUM_BINS: usize = 13;
36
37/// Size classes in bytes.
38const SIZE_CLASSES: [usize; NUM_BINS] = [
39    16, 32, 48, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 65536,
40];
41
42/// Find the bin index for a given allocation size.
43/// Returns `None` for sizes > 64 KB (overflow).
44fn bin_for_size(size: usize) -> Option<usize> {
45    for (i, &class_size) in SIZE_CLASSES.iter().enumerate() {
46        if size <= class_size {
47            return Some(i);
48        }
49    }
50    None
51}
52
53/// A single free-list bin. Stores pre-allocated blocks of a fixed size class.
54struct Bin {
55    /// Size class in bytes.
56    size_class: usize,
57    /// LIFO free list of block indices into `BinnedAllocator::storage`.
58    free_list: Vec<usize>,
59    /// Total blocks ever allocated for this bin.
60    total_allocated: usize,
61}
62
63impl Bin {
64    fn new(size_class: usize) -> Self {
65        Bin {
66            size_class,
67            free_list: Vec::new(),
68            total_allocated: 0,
69        }
70    }
71}
72
73/// Block metadata in the global storage.
74struct Block {
75    /// Offset into the backing buffer.
76    offset: usize,
77    /// Size class of this block.
78    size: usize,
79    /// Whether this block is currently in use.
80    in_use: bool,
81}
82
83/// Deterministic binned allocator with LIFO free lists.
84///
85/// All memory is backed by a single contiguous `Vec<u8>` that grows
86/// monotonically. No memory is ever returned to the OS during execution.
87pub struct BinnedAllocator {
88    /// Size-class bins (indexed by bin number).
89    bins: Vec<Bin>,
90    /// Global block registry.
91    blocks: Vec<Block>,
92    /// Backing storage — grows but never shrinks.
93    storage: Vec<u8>,
94    /// Overflow blocks (> 64 KB).
95    overflow_free: Vec<usize>,
96    /// Stats: total allocations.
97    pub alloc_count: usize,
98    /// Stats: total frees.
99    pub free_count: usize,
100}
101
102impl BinnedAllocator {
103    /// Create a new binned allocator.
104    pub fn new() -> Self {
105        let bins = SIZE_CLASSES.iter().map(|&s| Bin::new(s)).collect();
106        BinnedAllocator {
107            bins,
108            blocks: Vec::new(),
109            storage: Vec::new(),
110            overflow_free: Vec::new(),
111            alloc_count: 0,
112            free_count: 0,
113        }
114    }
115
116    /// Allocate a block of at least `size` bytes. Returns a block index.
117    ///
118    /// If a free block of the right size class exists, it is reused (LIFO).
119    /// Otherwise, a new block is carved from the backing storage.
120    pub fn alloc(&mut self, size: usize) -> usize {
121        self.alloc_count += 1;
122
123        if let Some(bin_idx) = bin_for_size(size) {
124            let bin = &mut self.bins[bin_idx];
125            // Try to reuse a free block.
126            if let Some(block_idx) = bin.free_list.pop() {
127                self.blocks[block_idx].in_use = true;
128                return block_idx;
129            }
130            // Allocate a new block.
131            let class_size = bin.size_class;
132            bin.total_allocated += 1;
133            self.alloc_new_block(class_size)
134        } else {
135            // Overflow: try reuse, else allocate.
136            if let Some(block_idx) = self.overflow_free.pop() {
137                if self.blocks[block_idx].size >= size {
138                    self.blocks[block_idx].in_use = true;
139                    return block_idx;
140                }
141                // Put it back — wrong size.
142                self.overflow_free.push(block_idx);
143            }
144            self.alloc_new_block(size)
145        }
146    }
147
148    /// Free a block, returning it to the appropriate bin's free list.
149    pub fn free(&mut self, block_idx: usize) {
150        if block_idx >= self.blocks.len() {
151            return;
152        }
153        let block = &mut self.blocks[block_idx];
154        if !block.in_use {
155            return; // Double-free protection.
156        }
157        block.in_use = false;
158        self.free_count += 1;
159
160        let size = block.size;
161        if let Some(bin_idx) = bin_for_size(size) {
162            self.bins[bin_idx].free_list.push(block_idx);
163        } else {
164            self.overflow_free.push(block_idx);
165        }
166    }
167
168    /// Get a slice to the block's memory.
169    pub fn get(&self, block_idx: usize) -> Option<&[u8]> {
170        self.blocks.get(block_idx).and_then(|b| {
171            if b.in_use {
172                self.storage.get(b.offset..b.offset + b.size)
173            } else {
174                None
175            }
176        })
177    }
178
179    /// Get a mutable slice to the block's memory.
180    pub fn get_mut(&mut self, block_idx: usize) -> Option<&mut [u8]> {
181        if let Some(b) = self.blocks.get(block_idx) {
182            if b.in_use {
183                let offset = b.offset;
184                let size = b.size;
185                return self.storage.get_mut(offset..offset + size);
186            }
187        }
188        None
189    }
190
191    /// Write bytes into a block.
192    pub fn write(&mut self, block_idx: usize, data: &[u8]) -> bool {
193        if let Some(slice) = self.get_mut(block_idx) {
194            let len = data.len().min(slice.len());
195            slice[..len].copy_from_slice(&data[..len]);
196            true
197        } else {
198            false
199        }
200    }
201
202    /// Number of blocks currently in use.
203    pub fn live_count(&self) -> usize {
204        self.blocks.iter().filter(|b| b.in_use).count()
205    }
206
207    /// Total blocks ever allocated.
208    pub fn total_blocks(&self) -> usize {
209        self.blocks.len()
210    }
211
212    /// Total backing storage in bytes (never shrinks).
213    pub fn storage_bytes(&self) -> usize {
214        self.storage.len()
215    }
216
217    /// Number of free blocks across all bins.
218    pub fn free_block_count(&self) -> usize {
219        self.bins.iter().map(|b| b.free_list.len()).sum::<usize>()
220            + self.overflow_free.len()
221    }
222
223    // Internal: allocate a new block from the backing storage.
224    fn alloc_new_block(&mut self, size: usize) -> usize {
225        let offset = self.storage.len();
226        // Align to 8 bytes.
227        let aligned = (size + 7) & !7;
228        self.storage.resize(self.storage.len() + aligned, 0);
229        let idx = self.blocks.len();
230        self.blocks.push(Block {
231            offset,
232            size: aligned,
233            in_use: true,
234        });
235        idx
236    }
237}
238
239impl Default for BinnedAllocator {
240    fn default() -> Self {
241        Self::new()
242    }
243}
244
245// ---------------------------------------------------------------------------
246// Tests
247// ---------------------------------------------------------------------------
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252
253    #[test]
254    fn test_bin_selection() {
255        assert_eq!(bin_for_size(1), Some(0));   // 16 B
256        assert_eq!(bin_for_size(16), Some(0));  // 16 B
257        assert_eq!(bin_for_size(17), Some(1));  // 32 B
258        assert_eq!(bin_for_size(48), Some(2));  // 48 B
259        assert_eq!(bin_for_size(65), Some(4));  // 128 B
260        assert_eq!(bin_for_size(65536), Some(12)); // 64 KB
261        assert_eq!(bin_for_size(65537), None);  // overflow
262    }
263
264    #[test]
265    fn test_alloc_and_read() {
266        let mut alloc = BinnedAllocator::new();
267        let b = alloc.alloc(8);
268        assert!(alloc.write(b, &[1, 2, 3, 4, 5, 6, 7, 8]));
269        assert_eq!(&alloc.get(b).unwrap()[..8], &[1, 2, 3, 4, 5, 6, 7, 8]);
270    }
271
272    #[test]
273    fn test_free_and_reuse() {
274        let mut alloc = BinnedAllocator::new();
275        let b1 = alloc.alloc(16);
276        alloc.free(b1);
277
278        // Next alloc of same size class should reuse b1's slot.
279        let b2 = alloc.alloc(16);
280        assert_eq!(b1, b2, "LIFO reuse");
281    }
282
283    #[test]
284    fn test_double_free_harmless() {
285        let mut alloc = BinnedAllocator::new();
286        let b = alloc.alloc(16);
287        alloc.free(b);
288        alloc.free(b); // should not panic
289        assert_eq!(alloc.free_count, 1, "second free should be no-op");
290    }
291
292    #[test]
293    fn test_storage_never_shrinks() {
294        let mut alloc = BinnedAllocator::new();
295        for _ in 0..100 {
296            let b = alloc.alloc(64);
297            alloc.free(b);
298        }
299        let bytes = alloc.storage_bytes();
300        // After many alloc/free cycles, storage should have grown.
301        assert!(bytes > 0);
302        // Storage never shrinks.
303        for _ in 0..100 {
304            let b = alloc.alloc(64);
305            alloc.free(b);
306        }
307        assert_eq!(alloc.storage_bytes(), bytes, "storage must not shrink");
308    }
309
310    #[test]
311    fn test_deterministic_alloc_order() {
312        let mut a1 = BinnedAllocator::new();
313        let mut a2 = BinnedAllocator::new();
314
315        let seq1: Vec<usize> = (0..10).map(|_| a1.alloc(32)).collect();
316        let seq2: Vec<usize> = (0..10).map(|_| a2.alloc(32)).collect();
317
318        assert_eq!(seq1, seq2, "same alloc sequence → same block indices");
319    }
320
321    #[test]
322    fn test_lifo_free_order() {
323        let mut alloc = BinnedAllocator::new();
324        let b1 = alloc.alloc(32);
325        let b2 = alloc.alloc(32);
326        let b3 = alloc.alloc(32);
327
328        // Free in order: b1, b2, b3
329        alloc.free(b1);
330        alloc.free(b2);
331        alloc.free(b3);
332
333        // LIFO: next alloc should return b3, then b2, then b1.
334        assert_eq!(alloc.alloc(32), b3);
335        assert_eq!(alloc.alloc(32), b2);
336        assert_eq!(alloc.alloc(32), b1);
337    }
338
339    #[test]
340    fn test_overflow_alloc() {
341        let mut alloc = BinnedAllocator::new();
342        let b = alloc.alloc(100_000); // > 64 KB
343        assert!(alloc.write(b, &[0xAB; 100]));
344        assert_eq!(alloc.get(b).unwrap()[0], 0xAB);
345    }
346
347    #[test]
348    fn test_live_count() {
349        let mut alloc = BinnedAllocator::new();
350        let b1 = alloc.alloc(16);
351        let b2 = alloc.alloc(32);
352        let _b3 = alloc.alloc(64);
353        assert_eq!(alloc.live_count(), 3);
354
355        alloc.free(b1);
356        assert_eq!(alloc.live_count(), 2);
357
358        alloc.free(b2);
359        assert_eq!(alloc.live_count(), 1);
360    }
361
362    #[test]
363    fn test_freed_block_not_readable() {
364        let mut alloc = BinnedAllocator::new();
365        let b = alloc.alloc(16);
366        alloc.free(b);
367        assert!(alloc.get(b).is_none(), "freed block should not be readable");
368    }
369
370    #[test]
371    fn test_mixed_size_classes() {
372        let mut alloc = BinnedAllocator::new();
373        let small = alloc.alloc(8);
374        let medium = alloc.alloc(256);
375        let large = alloc.alloc(4096);
376
377        alloc.free(small);
378        alloc.free(medium);
379        alloc.free(large);
380
381        // Each reuses its own size class.
382        let s2 = alloc.alloc(8);
383        let m2 = alloc.alloc(256);
384        let l2 = alloc.alloc(4096);
385
386        assert_eq!(s2, small);
387        assert_eq!(m2, medium);
388        assert_eq!(l2, large);
389    }
390}