Skip to content

Commit bb79316

Browse files
committed
move arena3 to mempool3
1 parent b8abcbf commit bb79316

File tree

14 files changed

+1917
-234
lines changed

14 files changed

+1917
-234
lines changed

oscars/src/alloc/mempool3/alloc.rs

Lines changed: 412 additions & 0 deletions
Large diffs are not rendered by default.

oscars/src/alloc/mempool3/mod.rs

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
//! size-class memory pool. typed GC objects go into per size class slot pools
2+
//! where freed slots are recycled via a free list, raw byte allocations use
3+
//! separate bump pages
4+
5+
use core::{cell::Cell, ptr::NonNull};
6+
use rust_alloc::alloc::{Layout, LayoutError};
7+
use rust_alloc::vec::Vec;
8+
9+
mod alloc;
10+
11+
use alloc::{BumpPage, SlotPool};
12+
pub use alloc::{ErasedPoolPointer, PoolItem, PoolPointer};
13+
14+
#[cfg(test)]
15+
mod tests;
16+
17+
#[derive(Debug, Clone)]
18+
pub enum PoolAllocError {
19+
LayoutError(LayoutError),
20+
OutOfMemory,
21+
AlignmentNotPossible,
22+
}
23+
24+
impl From<LayoutError> for PoolAllocError {
25+
fn from(value: LayoutError) -> Self {
26+
Self::LayoutError(value)
27+
}
28+
}
29+
30+
const SIZE_CLASSES: &[usize] = &[16, 24, 32, 48, 64, 96, 128, 192, 256, 512, 1024, 2048];
31+
32+
fn size_class_index_for(size: usize) -> usize {
33+
let idx = SIZE_CLASSES.iter().copied().position(|sc| sc >= size);
34+
debug_assert!(
35+
idx.is_some(),
36+
"object size {size}B exceeds the largest size class ({}B); \
37+
consider adding a larger class",
38+
SIZE_CLASSES.last().unwrap()
39+
);
40+
idx.unwrap_or(SIZE_CLASSES.len() - 1)
41+
}
42+
43+
const DEFAULT_PAGE_SIZE: usize = 4096;
44+
const DEFAULT_HEAP_THRESHOLD: usize = 2_097_152;
45+
46+
#[derive(Debug)]
47+
pub struct PoolAllocator<'alloc> {
48+
pub(crate) heap_threshold: usize,
49+
pub(crate) page_size: usize,
50+
pub(crate) current_heap_size: usize,
51+
// per size-class slot pools
52+
pub(crate) slot_pools: Vec<SlotPool>,
53+
// bump pages for raw byte allocs
54+
pub(crate) bump_pages: Vec<BumpPage>,
55+
// cached index of the last pool used by free_slot
56+
pub(crate) free_cache: Cell<usize>,
57+
// per size class cached index of the last pool used by alloc_slot
58+
pub(crate) alloc_cache: [Cell<usize>; 12],
59+
_marker: core::marker::PhantomData<&'alloc ()>,
60+
}
61+
62+
impl<'alloc> Default for PoolAllocator<'alloc> {
63+
fn default() -> Self {
64+
Self {
65+
heap_threshold: DEFAULT_HEAP_THRESHOLD,
66+
page_size: DEFAULT_PAGE_SIZE,
67+
current_heap_size: 0,
68+
slot_pools: Vec::new(),
69+
bump_pages: Vec::new(),
70+
free_cache: Cell::new(usize::MAX),
71+
alloc_cache: [
72+
Cell::new(usize::MAX),
73+
Cell::new(usize::MAX),
74+
Cell::new(usize::MAX),
75+
Cell::new(usize::MAX),
76+
Cell::new(usize::MAX),
77+
Cell::new(usize::MAX),
78+
Cell::new(usize::MAX),
79+
Cell::new(usize::MAX),
80+
Cell::new(usize::MAX),
81+
Cell::new(usize::MAX),
82+
Cell::new(usize::MAX),
83+
Cell::new(usize::MAX),
84+
],
85+
_marker: core::marker::PhantomData,
86+
}
87+
}
88+
}
89+
90+
impl<'alloc> PoolAllocator<'alloc> {
91+
pub fn with_page_size(mut self, page_size: usize) -> Self {
92+
self.page_size = page_size;
93+
self
94+
}
95+
pub fn with_heap_threshold(mut self, heap_threshold: usize) -> Self {
96+
self.heap_threshold = heap_threshold;
97+
self
98+
}
99+
100+
// total live slot pool + bump page count
101+
pub fn pools_len(&self) -> usize {
102+
self.slot_pools.len() + self.bump_pages.len()
103+
}
104+
105+
// exact heap size in bytes
106+
fn heap_size(&self) -> usize {
107+
self.current_heap_size
108+
}
109+
110+
pub fn is_below_threshold(&self) -> bool {
111+
// keep 25% headroom so collection fires before the last page fills
112+
let margin = self.heap_threshold / 4;
113+
self.heap_size() <= self.heap_threshold.saturating_sub(margin)
114+
}
115+
116+
pub fn increase_threshold(&mut self) {
117+
self.heap_threshold += self.page_size * 4;
118+
}
119+
}
120+
121+
impl<'alloc> PoolAllocator<'alloc> {
122+
pub fn try_alloc<T>(&mut self, value: T) -> Result<PoolPointer<'alloc, T>, PoolAllocError> {
123+
let needed = core::mem::size_of::<PoolItem<T>>().max(8);
124+
let sc_idx = size_class_index_for(needed);
125+
let slot_size = SIZE_CLASSES.get(sc_idx).copied().unwrap_or(needed);
126+
127+
let cached_idx = self.alloc_cache[sc_idx].get();
128+
if cached_idx < self.slot_pools.len() {
129+
let pool = &self.slot_pools[cached_idx];
130+
if pool.slot_size == slot_size {
131+
if let Some(slot_ptr) = pool.alloc_slot() {
132+
// SAFETY: slot_ptr was successfully allocated for this size class
133+
return unsafe {
134+
let dst = slot_ptr.as_ptr() as *mut PoolItem<T>;
135+
dst.write(PoolItem(value));
136+
Ok(PoolPointer::from_raw(NonNull::new_unchecked(dst)))
137+
};
138+
}
139+
}
140+
}
141+
142+
// try existing pools with matching slot_size first
143+
for (i, pool) in self.slot_pools.iter().enumerate().rev() {
144+
if pool.slot_size == slot_size {
145+
if let Some(slot_ptr) = pool.alloc_slot() {
146+
self.alloc_cache[sc_idx].set(i);
147+
// SAFETY: slot_ptr was successfully allocated for this size class
148+
return unsafe {
149+
let dst = slot_ptr.as_ptr() as *mut PoolItem<T>;
150+
dst.write(PoolItem(value));
151+
Ok(PoolPointer::from_raw(NonNull::new_unchecked(dst)))
152+
};
153+
}
154+
}
155+
}
156+
157+
// need a new pool for this size class
158+
let total = self.page_size.max(slot_size * 4);
159+
let new_pool = SlotPool::try_init(slot_size, total, 16)?;
160+
self.current_heap_size += new_pool.layout.size();
161+
let slot_ptr = new_pool.alloc_slot().ok_or(PoolAllocError::OutOfMemory)?;
162+
let insert_idx = self.slot_pools.len();
163+
self.slot_pools.push(new_pool);
164+
self.alloc_cache[sc_idx].set(insert_idx);
165+
166+
// SAFETY: slot_ptr was successfully allocated for this size class
167+
unsafe {
168+
let dst = slot_ptr.as_ptr() as *mut PoolItem<T>;
169+
dst.write(PoolItem(value));
170+
Ok(PoolPointer::from_raw(NonNull::new_unchecked(dst)))
171+
}
172+
}
173+
174+
// drops the value at `ptr` and returns the slot to the allocator
175+
//
176+
// SAFETY:
177+
// `ptr` must be a live `PoolItem<T>` allocated by this allocator,
178+
// must not be used after this call
179+
pub unsafe fn free_slot_typed<T>(&mut self, ptr: NonNull<PoolItem<T>>) {
180+
// SAFETY: guaranteed by caller
181+
unsafe { core::ptr::drop_in_place(ptr.as_ptr()) };
182+
self.free_slot(ptr.cast::<u8>());
183+
}
184+
185+
pub fn free_slot(&mut self, ptr: NonNull<u8>) {
186+
let cached = self.free_cache.get();
187+
if cached < self.slot_pools.len() {
188+
let pool = &self.slot_pools[cached];
189+
if pool.owns(ptr) {
190+
pool.free_slot(ptr);
191+
return;
192+
}
193+
}
194+
195+
for (i, pool) in self.slot_pools.iter().enumerate().rev() {
196+
if pool.owns(ptr) {
197+
pool.free_slot(ptr);
198+
self.free_cache.set(i);
199+
return;
200+
}
201+
}
202+
debug_assert!(
203+
false,
204+
"free_slot called with pointer {ptr:p} not owned by any slot pool; \
205+
possible double-free or pointer from a raw page"
206+
);
207+
}
208+
209+
// bump allocate raw bytes onto a BumpPage
210+
pub fn try_alloc_bytes(&mut self, layout: Layout) -> Result<NonNull<[u8]>, PoolAllocError> {
211+
// try the most recent bump page first
212+
if let Some(page) = self.bump_pages.last() {
213+
if let Ok(ptr) = page.try_alloc(layout) {
214+
return Ok(ptr);
215+
}
216+
}
217+
// allocate a new bump page with margin for padding
218+
let margin = 64;
219+
let total = self.page_size.max(layout.size() + layout.align() + margin);
220+
let max_align = layout.align().max(16);
221+
let page = BumpPage::try_init(total, max_align)?;
222+
self.current_heap_size += page.layout.size();
223+
let ptr = page
224+
.try_alloc(layout)
225+
.map_err(|_| PoolAllocError::OutOfMemory)?;
226+
self.bump_pages.push(page);
227+
Ok(ptr)
228+
}
229+
230+
// decrement live allocation count for the page owning ptr
231+
pub fn dealloc_bytes(&mut self, ptr: NonNull<u8>) {
232+
for page in self.bump_pages.iter().rev() {
233+
if page.owns(ptr) {
234+
page.dealloc();
235+
return;
236+
}
237+
}
238+
}
239+
240+
// try to shrink a raw allocation in place
241+
pub fn shrink_bytes_in_place(
242+
&mut self,
243+
ptr: NonNull<u8>,
244+
old_layout: Layout,
245+
new_layout: Layout,
246+
) -> bool {
247+
for page in self.bump_pages.iter().rev() {
248+
if page.owns(ptr) {
249+
return page.shrink_in_place(ptr, old_layout, new_layout);
250+
}
251+
}
252+
false
253+
}
254+
255+
// try to grow a raw allocation in place
256+
pub fn grow_bytes_in_place(
257+
&mut self,
258+
ptr: NonNull<u8>,
259+
old_layout: Layout,
260+
new_layout: Layout,
261+
) -> bool {
262+
for page in self.bump_pages.iter().rev() {
263+
if page.owns(ptr) {
264+
return page.grow_in_place(ptr, old_layout, new_layout);
265+
}
266+
}
267+
false
268+
}
269+
270+
// drop empty slot pools and bump pages
271+
pub fn drop_empty_pools(&mut self) {
272+
self.slot_pools.retain(|p| {
273+
if p.run_drop_check() {
274+
self.current_heap_size = self.current_heap_size.saturating_sub(p.layout.size());
275+
false
276+
} else {
277+
true
278+
}
279+
});
280+
self.bump_pages.retain(|p| {
281+
if p.run_drop_check() {
282+
self.current_heap_size = self.current_heap_size.saturating_sub(p.layout.size());
283+
false
284+
} else {
285+
true
286+
}
287+
});
288+
self.free_cache.set(usize::MAX);
289+
for cache in &self.alloc_cache {
290+
cache.set(usize::MAX);
291+
}
292+
}
293+
294+
// mark the slot at `ptr` as occupied (only slot pools have a bitmap)
295+
pub fn mark_slot(&self, ptr: NonNull<u8>) {
296+
for pool in self.slot_pools.iter() {
297+
if pool.owns(ptr) {
298+
pool.mark_slot(ptr);
299+
return;
300+
}
301+
}
302+
}
303+
}

0 commit comments

Comments
 (0)