use std::alloc::{Layout, alloc, dealloc};
use std::collections::HashMap;
use std::ptr;
use crate::align_up;
pub struct FreelistBump {
ptr: *mut u8,
layout: Layout,
cursor: usize,
buckets: HashMap<(usize, usize), Vec<*mut u8>>,
reuse_hits: u64,
}
impl FreelistBump {
pub fn new() -> Self {
Self::with_capacity(4096)
}
pub fn with_capacity(capacity: usize) -> Self {
let capacity = capacity.max(64);
let layout = Layout::from_size_align(capacity, 16).expect("layout");
let ptr = unsafe { alloc(layout) };
assert!(!ptr.is_null(), "OOM allocating freelist arena chunk");
Self {
ptr,
layout,
cursor: 0,
buckets: HashMap::new(),
reuse_hits: 0,
}
}
pub fn alloc_copy<T: Copy>(&mut self, value: T) -> &mut T {
let layout = Layout::new::<T>();
let p = self.alloc_raw(layout);
unsafe {
ptr::write(p as *mut T, value);
&mut *(p as *mut T)
}
}
pub fn alloc_raw(&mut self, layout: Layout) -> *mut u8 {
let key = (layout.size(), layout.align());
if let Some(stack) = self.buckets.get_mut(&key) {
if let Some(p) = stack.pop() {
self.reuse_hits += 1;
return p;
}
}
self.bump_alloc_raw(layout)
.expect("FreelistBump out of capacity")
}
fn bump_alloc_raw(&mut self, layout: Layout) -> Option<*mut u8> {
let size = layout.size();
let align = layout.align();
let base = self.ptr as usize;
let aligned = align_up(base + self.cursor, align) - base;
let end = aligned.checked_add(size)?;
if end > self.layout.size() {
return None;
}
self.cursor = end;
Some(unsafe { self.ptr.add(aligned) })
}
pub unsafe fn free(&mut self, ptr_to_free: *mut u8, layout: Layout) {
debug_assert!(
self.contains(ptr_to_free),
"FreelistBump::free given pointer outside the arena",
);
let key = (layout.size(), layout.align());
self.buckets.entry(key).or_default().push(ptr_to_free);
}
fn contains(&self, p: *mut u8) -> bool {
let base = self.ptr as usize;
let end = base + self.layout.size();
let q = p as usize;
q >= base && q < end
}
pub fn reset(&mut self) {
self.buckets.clear();
self.cursor = 0;
self.reuse_hits = 0;
}
pub fn used(&self) -> usize {
self.cursor
}
pub fn capacity(&self) -> usize {
self.layout.size()
}
pub fn reuse_hits(&self) -> u64 {
self.reuse_hits
}
pub fn freelist_len(&self) -> usize {
self.buckets.values().map(|v| v.len()).sum()
}
}
impl Default for FreelistBump {
fn default() -> Self {
Self::new()
}
}
impl Drop for FreelistBump {
fn drop(&mut self) {
unsafe { dealloc(self.ptr, self.layout) };
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fresh_alloc_bumps_cursor() {
let mut a = FreelistBump::with_capacity(256);
let _ = a.alloc_copy(123u32);
assert!(a.used() >= 4);
assert_eq!(a.reuse_hits(), 0);
}
#[test]
fn freed_slot_is_reused() {
let mut a = FreelistBump::with_capacity(256);
let layout = Layout::new::<u32>();
let p1 = a.alloc_raw(layout);
let used_after_first = a.used();
unsafe { a.free(p1, layout) };
let p2 = a.alloc_raw(layout);
assert_eq!(p1, p2, "freed slot must be reused");
assert_eq!(
a.used(),
used_after_first,
"reusing should not advance cursor"
);
assert_eq!(a.reuse_hits(), 1);
}
#[test]
fn freelist_is_per_size() {
let mut a = FreelistBump::with_capacity(256);
let l32 = Layout::new::<u32>();
let l64 = Layout::new::<u64>();
let p32 = a.alloc_raw(l32);
unsafe { a.free(p32, l32) };
let p64 = a.alloc_raw(l64);
assert_ne!(p32, p64);
assert_eq!(a.reuse_hits(), 0);
}
#[test]
fn many_frees_reused_lifo() {
let mut a = FreelistBump::with_capacity(1024);
let layout = Layout::new::<u64>();
let p1 = a.alloc_raw(layout);
let p2 = a.alloc_raw(layout);
let p3 = a.alloc_raw(layout);
unsafe {
a.free(p1, layout);
a.free(p2, layout);
a.free(p3, layout);
}
assert_eq!(a.freelist_len(), 3);
let r1 = a.alloc_raw(layout);
let r2 = a.alloc_raw(layout);
let r3 = a.alloc_raw(layout);
assert_eq!(r1, p3);
assert_eq!(r2, p2);
assert_eq!(r3, p1);
assert_eq!(a.reuse_hits(), 3);
assert_eq!(a.freelist_len(), 0);
}
#[test]
fn reset_clears_freelist_and_cursor() {
let mut a = FreelistBump::with_capacity(256);
let layout = Layout::new::<u32>();
let p = a.alloc_raw(layout);
unsafe { a.free(p, layout) };
assert_eq!(a.freelist_len(), 1);
a.reset();
assert_eq!(a.freelist_len(), 0);
assert_eq!(a.used(), 0);
assert_eq!(a.reuse_hits(), 0);
}
#[test]
fn reuse_under_64_byte_alignment() {
let mut a = FreelistBump::with_capacity(512);
let layout = Layout::from_size_align(64, 64).expect("layout");
let p1 = a.alloc_raw(layout);
assert_eq!(p1 as usize % 64, 0);
unsafe { a.free(p1, layout) };
let p2 = a.alloc_raw(layout);
assert_eq!(p1, p2);
assert_eq!(p2 as usize % 64, 0);
}
}