Skip to main content

oxide_core/heap/
arena.rs

1//! `Arena<T>`: a `Vec<T>` with a side live bitmap and a side mark bitmap.
2//!
3//! The live bitmap is what sweep uses to tell free slots from dead records,
4//! and allocation takes the lowest clear bit at or after a cursor, so reuse is
5//! lowest index first and deterministic. Only debug and `gc-stress` builds
6//! assert liveness on access; release builds check nothing.
7//!
8//! The record vector grows by [`step_bytes`] and gives back a trailing free
9//! run of more than a quarter of its capacity after a sweep.
10
11use std::mem;
12
13use super::gc::Room;
14
15/// A record stored in an [`Arena`].
16pub trait Record {
17    /// The poison value written over a freed slot. It must own no heap memory.
18    fn vacant() -> Self;
19
20    /// Bytes owned outside the arena slot (string contents, tables), counted
21    /// toward the heap cap.
22    fn extra_bytes(&self) -> usize {
23        0
24    }
25}
26
27const CHECK_LIVE: bool = cfg!(any(debug_assertions, feature = "gc-stress"));
28
29/// Below this many bytes a vector doubles. The allocator keeps such blocks
30/// in its heaps, where growing one copies it and leaves the old block
31/// resident; a doubling makes that happen once per doubling. Larger blocks
32/// are each their own mapping and grow in place (`mremap`), so they grow by
33/// [`STEP`] and their slack stays below one step.
34pub const DOUBLING_LIMIT: usize = 32 << 20;
35pub const STEP: usize = 2 << 20;
36
37/// The bytes a vector of `capacity` bytes grows by.
38#[inline]
39pub fn step_bytes(capacity: usize, unit: usize) -> usize {
40    if capacity < DOUBLING_LIMIT {
41        capacity.max(4 * unit)
42    } else {
43        STEP
44    }
45}
46
47/// The capacity, in elements of `unit` bytes, after growing from `capacity`
48/// until it holds `needed`.
49pub fn grown_capacity(mut capacity: usize, needed: usize, unit: usize) -> usize {
50    while capacity < needed {
51        capacity += step_bytes(capacity * unit, unit).div_ceil(unit);
52    }
53    capacity
54}
55
56pub struct Arena<T> {
57    records: Vec<T>,
58    live: Vec<u64>,
59    marks: Vec<u64>,
60    /// Slots below `records.len()` that are free and reusable.
61    free_count: usize,
62    /// No reusable free slot lies below word `cursor` of `live`.
63    cursor: usize,
64    /// Under `gc-stress`, slots freed by a sweep are reusable only after the
65    /// next sweep, so a stale index is not immediately recycled into a live
66    /// record and the liveness assertion catches it.
67    #[cfg(feature = "gc-stress")]
68    quarantine: Vec<u64>,
69    live_count: usize,
70    extra: usize,
71}
72
73#[inline]
74fn bit(index: u32) -> (usize, u64) {
75    ((index / 64) as usize, 1u64 << (index % 64))
76}
77
78impl<T: Record> Default for Arena<T> {
79    fn default() -> Self {
80        Self::new()
81    }
82}
83
84impl<T: Record> Arena<T> {
85    pub fn new() -> Self {
86        Arena {
87            records: Vec::new(),
88            live: Vec::new(),
89            marks: Vec::new(),
90            free_count: 0,
91            cursor: 0,
92            #[cfg(feature = "gc-stress")]
93            quarantine: Vec::new(),
94            live_count: 0,
95            extra: 0,
96        }
97    }
98
99    /// Stores `value` in the lowest free slot, or past the last one. Returns
100    /// its index. A growth step is charged to `room`.
101    #[inline(always)]
102    pub fn alloc(&mut self, value: T, room: &mut Room) -> u32 {
103        self.extra += value.extra_bytes();
104        self.live_count += 1;
105        if self.free_count != 0 {
106            let index = self.take_free();
107            self.records[index as usize] = value;
108            return index;
109        }
110        self.push(value, room)
111    }
112
113    /// The slots of word `word` that are not reusable: live, or quarantined.
114    #[inline(always)]
115    fn taken(&self, word: usize) -> u64 {
116        #[cfg(feature = "gc-stress")]
117        return self.live[word] | self.quarantine[word];
118        #[cfg(not(feature = "gc-stress"))]
119        self.live[word]
120    }
121
122    /// The lowest free slot at or after the cursor, marked live.
123    #[inline(always)]
124    fn take_free(&mut self) -> u32 {
125        let mut word = self.cursor;
126        loop {
127            let taken = self.taken(word);
128            if taken != u64::MAX {
129                let low = (!taken).trailing_zeros();
130                self.live[word] |= 1 << low;
131                self.cursor = word;
132                self.free_count -= 1;
133                let index = word as u32 * 64 + low;
134                debug_assert!((index as usize) < self.records.len());
135                return index;
136            }
137            word += 1;
138        }
139    }
140
141    #[inline(never)]
142    fn push(&mut self, value: T, room: &mut Room) -> u32 {
143        let index = u32::try_from(self.records.len()).expect("arena index overflow");
144        if self.records.len() == self.records.capacity() {
145            let before = self.bytes_without_extra();
146            let unit = mem::size_of::<T>().max(1);
147            let old = self.records.capacity();
148            let capacity = grown_capacity(old, old + 1, unit);
149            self.records.reserve_exact(capacity - old);
150            let words = self.records.capacity().div_ceil(64);
151            self.live.reserve_exact(words - self.live.len());
152            self.marks.reserve_exact(words - self.marks.len());
153            #[cfg(feature = "gc-stress")]
154            self.quarantine.reserve_exact(words - self.quarantine.len());
155            room.take(self.bytes_without_extra() - before);
156        }
157        self.records.push(value);
158        let (word, mask) = bit(index);
159        if word == self.live.len() {
160            self.live.push(0);
161            self.marks.push(0);
162            #[cfg(feature = "gc-stress")]
163            self.quarantine.push(0);
164        }
165        self.live[word] |= mask;
166        index
167    }
168
169    /// Bytes the arena's record vector grows by if `count` records are
170    /// allocated now (zero when its free slots and capacity cover them).
171    pub fn growth_bytes(&self, count: usize) -> usize {
172        // Quarantined slots do not count: they are not reusable yet.
173        let needed = self.records.len() + count.saturating_sub(self.free_count);
174        let old = self.records.capacity();
175        let unit = mem::size_of::<T>().max(1);
176        (grown_capacity(old, needed, unit) - old) * unit
177    }
178
179    #[inline]
180    pub fn is_live(&self, index: u32) -> bool {
181        let (word, mask) = bit(index);
182        self.live.get(word).is_some_and(|bits| bits & mask != 0)
183    }
184
185    #[inline]
186    #[track_caller]
187    fn check(&self, index: u32) {
188        if CHECK_LIVE {
189            assert!(
190                self.is_live(index),
191                "stale {} index {index}",
192                std::any::type_name::<T>()
193            );
194        }
195    }
196
197    #[inline]
198    #[track_caller]
199    pub fn get(&self, index: u32) -> &T {
200        self.check(index);
201        &self.records[index as usize]
202    }
203
204    #[inline]
205    #[track_caller]
206    pub fn get_mut(&mut self, index: u32) -> &mut T {
207        self.check(index);
208        &mut self.records[index as usize]
209    }
210
211    /// Frees one record: poisons it, clears its live bit and returns it.
212    #[track_caller]
213    pub fn free(&mut self, index: u32) -> T {
214        assert!(self.is_live(index), "double free of index {index}");
215        let record = mem::replace(&mut self.records[index as usize], T::vacant());
216        self.extra -= record.extra_bytes();
217        self.live_count -= 1;
218        let (word, mask) = bit(index);
219        self.live[word] &= !mask;
220        self.free_count += 1;
221        self.cursor = self.cursor.min(word);
222        record
223    }
224
225    /// Records whose extra bytes change in place report it here.
226    pub fn adjust_extra(&mut self, old: usize, new: usize) {
227        self.extra = self.extra - old + new;
228    }
229
230    pub fn live_count(&self) -> usize {
231        self.live_count
232    }
233
234    /// Number of slots, live or free.
235    pub fn slot_count(&self) -> usize {
236        self.records.len()
237    }
238
239    /// Bytes of record storage and bitmaps.
240    fn bytes_without_extra(&self) -> usize {
241        #[allow(unused_mut)]
242        let mut words = self.live.capacity() + self.marks.capacity();
243        #[cfg(feature = "gc-stress")]
244        {
245            words += self.quarantine.capacity();
246        }
247        self.records.capacity() * mem::size_of::<T>() + words * 8
248    }
249
250    /// Bytes reserved by the arena: record storage, bitmaps and the records'
251    /// extra bytes.
252    pub fn bytes(&self) -> usize {
253        self.bytes_without_extra() + self.extra
254    }
255
256    /// Bytes held by live records.
257    pub fn used_bytes(&self) -> usize {
258        self.live_count * mem::size_of::<T>() + self.extra
259    }
260
261    /// Sets the mark bit; returns true if it was clear.
262    #[inline]
263    #[track_caller]
264    pub fn mark(&mut self, index: u32) -> bool {
265        self.check(index);
266        let (word, mask) = bit(index);
267        let bits = &mut self.marks[word];
268        let fresh = *bits & mask == 0;
269        *bits |= mask;
270        fresh
271    }
272
273    #[inline]
274    pub fn is_marked(&self, index: u32) -> bool {
275        let (word, mask) = bit(index);
276        self.marks.get(word).is_some_and(|bits| bits & mask != 0)
277    }
278
279    pub fn clear_marks(&mut self) {
280        self.marks.fill(0);
281    }
282
283    /// Frees every live, unmarked record, highest index first, passing each to
284    /// `dead`, then gives back a long trailing free run. Returns the number
285    /// freed.
286    pub fn sweep(&mut self, mut dead: impl FnMut(u32, T)) -> usize {
287        #[cfg(feature = "gc-stress")]
288        self.release_quarantine();
289        let mut freed = 0;
290        let mut lowest = self.live.len();
291        for word in (0..self.live.len()).rev() {
292            let mut garbage = self.live[word] & !self.marks[word];
293            if garbage == 0 {
294                continue;
295            }
296            lowest = word;
297            self.live[word] &= !garbage;
298            #[cfg(feature = "gc-stress")]
299            {
300                self.quarantine[word] = garbage;
301            }
302            while garbage != 0 {
303                let high = 63 - garbage.leading_zeros();
304                garbage &= !(1u64 << high);
305                let index = word as u32 * 64 + high;
306                let record = mem::replace(&mut self.records[index as usize], T::vacant());
307                self.extra -= record.extra_bytes();
308                freed += 1;
309                dead(index, record);
310            }
311        }
312        self.live_count -= freed;
313        if !cfg!(feature = "gc-stress") {
314            self.free_count += freed;
315            self.cursor = self.cursor.min(lowest);
316        }
317        self.shrink();
318        freed
319    }
320
321    /// Makes the slots the last sweep freed reusable.
322    #[cfg(feature = "gc-stress")]
323    fn release_quarantine(&mut self) {
324        for (word, bits) in self.quarantine.iter_mut().enumerate() {
325            if *bits != 0 {
326                self.free_count += bits.count_ones() as usize;
327                self.cursor = self.cursor.min(word);
328                *bits = 0;
329            }
330        }
331    }
332
333    /// Truncates a trailing free run of more than a quarter of the capacity
334    /// and returns the memory past one growth step above the rest.
335    /// Quarantined slots stay, so a stale index is not reused at once.
336    fn shrink(&mut self) {
337        let capacity = self.records.capacity();
338        let end = match (0..self.live.len())
339            .rev()
340            .find(|&word| self.taken(word) != 0)
341        {
342            Some(word) => word * 64 + 64 - self.taken(word).leading_zeros() as usize,
343            None => 0,
344        };
345        if (capacity - end) * 4 <= capacity {
346            return;
347        }
348        // Every slot past `end` is free and reusable.
349        self.free_count -= self.records.len() - end;
350        let words = end.div_ceil(64);
351        self.records.truncate(end);
352        self.live.truncate(words);
353        self.marks.truncate(words);
354        #[cfg(feature = "gc-stress")]
355        self.quarantine.truncate(words);
356        self.cursor = self.cursor.min(words);
357        let unit = mem::size_of::<T>().max(1);
358        self.records
359            .shrink_to(end + step_bytes(end * unit, unit).div_ceil(unit));
360        let words = self.records.capacity().div_ceil(64);
361        self.live.shrink_to(words);
362        self.marks.shrink_to(words);
363        #[cfg(feature = "gc-stress")]
364        self.quarantine.shrink_to(words);
365    }
366
367    /// Live indices in ascending order.
368    pub fn live_indices(&self) -> impl Iterator<Item = u32> + '_ {
369        self.live.iter().enumerate().flat_map(|(word, &bits)| {
370            let mut bits = bits;
371            std::iter::from_fn(move || {
372                if bits == 0 {
373                    return None;
374                }
375                let low = bits.trailing_zeros();
376                bits &= bits - 1;
377                Some(word as u32 * 64 + low)
378            })
379        })
380    }
381
382    /// Free indices below the last slot, including quarantined ones.
383    pub fn free_indices(&self) -> impl Iterator<Item = u32> + '_ {
384        (0..self.records.len() as u32).filter(|&index| !self.is_live(index))
385    }
386
387    /// Debug check: the free count and the cursor agree with the bitmap.
388    pub fn free_count_matches(&self) -> bool {
389        #[allow(unused_mut)]
390        let mut reusable = self.records.len() - self.live_count;
391        #[cfg(feature = "gc-stress")]
392        {
393            reusable -= self
394                .quarantine
395                .iter()
396                .map(|bits| bits.count_ones() as usize)
397                .sum::<usize>();
398        }
399        let below_cursor = (0..(self.cursor * 64).min(self.records.len()) as u32)
400            .filter(|&index| !self.is_live(index))
401            .count();
402        #[cfg(feature = "gc-stress")]
403        let below_cursor = below_cursor
404            - (0..self.cursor.min(self.quarantine.len()))
405                .map(|word| self.quarantine[word].count_ones() as usize)
406                .sum::<usize>();
407        reusable == self.free_count && below_cursor == 0
408    }
409
410    /// Debug check: the live and mark bitmaps agree.
411    pub fn marks_equal_live(&self) -> Option<u32> {
412        self.live
413            .iter()
414            .zip(&self.marks)
415            .enumerate()
416            .find_map(|(word, (live, marks))| {
417                let diff = live ^ marks;
418                (diff != 0).then(|| word as u32 * 64 + diff.trailing_zeros())
419            })
420    }
421}
422
423#[cfg(test)]
424mod tests {
425    use super::*;
426
427    #[derive(Debug, PartialEq)]
428    struct Item(u32);
429
430    impl Record for Item {
431        fn vacant() -> Self {
432            Item(u32::MAX)
433        }
434    }
435
436    #[test]
437    fn reuse_is_lowest_first_after_sweep() {
438        let mut room = Room::unbounded();
439        let mut arena = Arena::new();
440        for value in 0..10 {
441            assert_eq!(arena.alloc(Item(value), &mut room), value);
442        }
443        for keep in [0, 3, 9] {
444            arena.mark(keep);
445        }
446        let mut dead = Vec::new();
447        assert_eq!(arena.sweep(|index, item| dead.push((index, item.0))), 7);
448        assert_eq!(dead.first(), Some(&(8, 8)));
449        assert_eq!(arena.live_count(), 3);
450        assert_eq!(arena.live_indices().collect::<Vec<_>>(), [0, 3, 9]);
451        arena.clear_marks();
452        #[cfg(feature = "gc-stress")]
453        {
454            // A second sweep releases the quarantine.
455            for keep in [0, 3, 9] {
456                arena.mark(keep);
457            }
458            assert_eq!(arena.sweep(|_, _| {}), 0);
459        }
460        assert!(arena.free_count_matches());
461        let reused = (0..7)
462            .map(|value| arena.alloc(Item(100 + value), &mut room))
463            .collect::<Vec<_>>();
464        assert_eq!(reused, [1, 2, 4, 5, 6, 7, 8]);
465        assert_eq!(arena.alloc(Item(200), &mut room), 10);
466        arena.free(4);
467        arena.free(2);
468        assert_eq!(arena.alloc(Item(300), &mut room), 2);
469        assert_eq!(arena.alloc(Item(301), &mut room), 4);
470        assert!(arena.free_count_matches());
471    }
472
473    #[test]
474    fn free_poisons_and_growth_is_reported() {
475        let mut room = Room::new(1 << 20, 1 << 20);
476        let mut arena = Arena::new();
477        assert_eq!(arena.growth_bytes(1), 4 * size_of::<Item>());
478        let index = arena.alloc(Item(7), &mut room);
479        assert_eq!(room.threshold(), 1 << 20);
480        assert_eq!(arena.growth_bytes(3), 0);
481        assert_eq!(arena.free(index), Item(7));
482        assert!(!arena.is_live(index));
483        assert_eq!(arena.growth_bytes(4), 0);
484        assert_eq!(arena.growth_bytes(5), 4 * size_of::<Item>());
485    }
486
487    #[test]
488    fn growth_steps_are_bounded() {
489        assert_eq!(step_bytes(0, 24), 96);
490        assert_eq!(step_bytes(4 << 20, 24), 4 << 20);
491        assert_eq!(step_bytes(64 << 20, 24), STEP);
492        let mut room = Room::unbounded();
493        let mut arena = Arena::new();
494        for value in 0..10_000_000 {
495            arena.alloc(Item(value), &mut room);
496        }
497        let capacity = arena.records.capacity() * size_of::<Item>();
498        let used = arena.records.len() * size_of::<Item>();
499        assert!(capacity - used < STEP, "{capacity} for {used}");
500    }
501
502    #[test]
503    fn a_trailing_free_run_is_given_back() {
504        let mut room = Room::unbounded();
505        let mut arena = Arena::new();
506        for value in 0..100_000 {
507            arena.alloc(Item(value), &mut room);
508        }
509        let before = arena.bytes();
510        for keep in 0..1000 {
511            arena.mark(keep);
512        }
513        arena.sweep(|_, _| {});
514        #[cfg(feature = "gc-stress")]
515        {
516            // The quarantined slots stay until the next sweep releases them.
517            assert_eq!(arena.slot_count(), 100_000);
518            arena.clear_marks();
519            for keep in 0..1000 {
520                arena.mark(keep);
521            }
522            arena.sweep(|_, _| {});
523        }
524        assert_eq!(arena.slot_count(), 1000);
525        assert!(arena.bytes() < before / 4);
526        assert_eq!(arena.alloc(Item(5), &mut room), 1000);
527    }
528
529    #[test]
530    fn a_step_past_the_budget_zeroes_the_threshold() {
531        let mut room = Room::new(1 << 10, usize::MAX);
532        let mut arena = Arena::new();
533        let mut value = 0;
534        while room.threshold() != 0 {
535            assert!(arena.bytes() <= 1 << 10);
536            arena.alloc(Item(value), &mut room);
537            value += 1;
538        }
539        assert!(arena.bytes() > 1 << 10);
540    }
541
542    #[test]
543    #[cfg(any(debug_assertions, feature = "gc-stress"))]
544    #[should_panic(expected = "stale")]
545    fn stale_access_panics() {
546        let mut arena = Arena::new();
547        let index = arena.alloc(Item(1), &mut Room::unbounded());
548        arena.free(index);
549        let _ = arena.get(index);
550    }
551}