use core::alloc::Layout;
use core::ptr::NonNull;
mod arc;
pub(crate) use self::arc::BlockArc;
pub(crate) const STEP: usize = 64;
pub(crate) const CLASSES: usize = 16;
pub(crate) const KEPT_BYTES: usize = 16 * 1024;
fn kept(class: usize) -> usize {
KEPT_BYTES / block(class).size()
}
fn class_of(layout: Layout) -> Option<usize> {
(layout.align() <= STEP && layout.size() <= STEP * CLASSES && layout.size() > 0)
.then(|| layout.size().div_ceil(STEP) - 1)
}
fn block(class: usize) -> Layout {
unsafe { Layout::from_size_align_unchecked((class + 1) * STEP, STEP) }
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct Owner(*const cache::Record);
unsafe impl Send for Owner {}
unsafe impl Sync for Owner {}
impl Owner {
const NONE: Owner = Owner(core::ptr::null());
}
#[cfg(not(all(loom, test)))]
mod cache {
use core::cell::RefCell;
use core::ptr::{self, NonNull};
use core::sync::atomic::{AtomicBool, AtomicPtr, AtomicUsize, Ordering};
use super::{CLASSES, KEPT_BYTES, Owner, block, kept};
#[repr(C)]
struct Freed {
next: *mut Freed,
class: usize,
}
const CLOSED: *mut Freed = ptr::dangling_mut();
pub(super) struct Record {
head: AtomicPtr<Freed>,
bytes: AtomicUsize,
claimed: AtomicBool,
next: AtomicPtr<Record>,
}
static RECORDS: AtomicPtr<Record> = AtomicPtr::new(ptr::null_mut());
#[cfg(test)]
pub(super) static CLOSED_RECORD: Record = Record {
head: AtomicPtr::new(CLOSED),
bytes: AtomicUsize::new(0),
claimed: AtomicBool::new(true),
next: AtomicPtr::new(ptr::null_mut()),
};
fn claim() -> &'static Record {
let mut at = RECORDS.load(Ordering::Acquire);
while let Some(record) = unsafe { at.as_ref() } {
if !record.claimed.load(Ordering::Relaxed)
&& record
.claimed
.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
.is_ok()
{
record.head.store(ptr::null_mut(), Ordering::Relaxed);
return record;
}
at = record.next.load(Ordering::Relaxed);
}
let record: &'static Record = Box::leak(Box::new(Record {
head: AtomicPtr::new(ptr::null_mut()),
bytes: AtomicUsize::new(0),
claimed: AtomicBool::new(true),
next: AtomicPtr::new(ptr::null_mut()),
}));
let mut last = RECORDS.load(Ordering::Acquire);
loop {
record.next.store(last, Ordering::Relaxed);
match RECORDS.compare_exchange_weak(
last,
ptr::from_ref(record).cast_mut(),
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => return record,
Err(now) => last = now,
}
}
}
fn give_back(record: &Record, ptr: NonNull<u8>, class: usize) -> bool {
let size = block(class).size();
if record.bytes.fetch_add(size, Ordering::Relaxed) + size > KEPT_BYTES {
record.bytes.fetch_sub(size, Ordering::Relaxed);
return false;
}
let freed = ptr.cast::<Freed>().as_ptr();
let mut head = record.head.load(Ordering::Relaxed);
loop {
if head == CLOSED {
record.bytes.fetch_sub(size, Ordering::Relaxed);
return false;
}
unsafe { freed.write(Freed { next: head, class }) };
match record.head.compare_exchange_weak(
head,
freed,
Ordering::Release,
Ordering::Relaxed,
) {
Ok(_) => return true,
Err(now) => head = now,
}
}
}
pub(super) struct Cache {
lists: [Vec<NonNull<u8>>; CLASSES],
record: Option<&'static Record>,
}
impl Cache {
fn keep(&mut self, class: usize, ptr: NonNull<u8>) -> bool {
let blocks = &mut self.lists[class];
if blocks.len() >= kept(class) {
return false;
}
if blocks.capacity() == 0 {
blocks.reserve_exact(kept(class));
}
blocks.push(ptr);
true
}
fn take_back(&mut self, record: &Record) {
if record.head.load(Ordering::Relaxed).is_null() {
return;
}
let mut list = record.head.swap(ptr::null_mut(), Ordering::Acquire);
let mut taken = 0;
while !list.is_null() {
let Freed { next, class } = unsafe { list.read() };
let ptr = unsafe { NonNull::new_unchecked(list.cast::<u8>()) };
if !self.keep(class, ptr) {
unsafe { std::alloc::dealloc(ptr.as_ptr(), block(class)) };
}
taken += block(class).size();
list = next;
}
record.bytes.fetch_sub(taken, Ordering::Relaxed);
}
}
impl Drop for Cache {
fn drop(&mut self) {
if let Some(record) = self.record {
let mut list = record.head.swap(CLOSED, Ordering::Acquire);
let mut taken = 0;
while !list.is_null() {
let Freed { next, class } = unsafe { list.read() };
unsafe { std::alloc::dealloc(list.cast(), block(class)) };
taken += block(class).size();
list = next;
}
record.bytes.fetch_sub(taken, Ordering::Relaxed);
}
for (class, blocks) in self.lists.iter_mut().enumerate() {
for ptr in blocks.drain(..) {
unsafe { std::alloc::dealloc(ptr.as_ptr(), block(class)) };
}
}
if let Some(record) = self.record {
record.claimed.store(false, Ordering::Release);
}
}
}
thread_local! {
static CACHE: RefCell<Cache> = const {
RefCell::new(Cache {
lists: [const { Vec::new() }; CLASSES],
record: None,
})
};
}
pub(super) fn take(class: usize) -> (Option<NonNull<u8>>, Owner) {
CACHE
.try_with(|cache| {
let Ok(mut cache) = cache.try_borrow_mut() else {
return (None, Owner::NONE);
};
let record = *cache.record.get_or_insert_with(claim);
if cache.lists[class].is_empty() {
cache.take_back(record);
}
(cache.lists[class].pop(), Owner(record))
})
.unwrap_or((None, Owner::NONE))
}
pub(super) fn keep(class: usize, ptr: NonNull<u8>, owner: Owner) -> bool {
let owner = unsafe { owner.0.as_ref() };
CACHE
.try_with(|cache| {
let mut cache = cache.try_borrow_mut().ok();
let mine = cache
.as_ref()
.and_then(|cache| cache.record)
.is_some_and(|record| owner.is_some_and(|o| ptr::eq(o, record)));
if !mine
&& let Some(owner) = owner
&& give_back(owner, ptr, class)
{
return true;
}
cache.as_mut().is_some_and(|cache| cache.keep(class, ptr))
})
.unwrap_or_else(|_| owner.is_some_and(|o| give_back(o, ptr, class)))
}
}
#[cfg(all(loom, test))]
mod cache {
use core::ptr::NonNull;
use super::Owner;
pub(super) struct Record;
pub(super) fn take(_class: usize) -> (Option<NonNull<u8>>, Owner) {
(None, Owner::NONE)
}
pub(super) fn keep(_class: usize, _ptr: NonNull<u8>, _owner: Owner) -> bool {
false
}
}
pub(crate) fn alloc(layout: Layout) -> (NonNull<u8>, Owner) {
let (layout, owner) = match class_of(layout) {
Some(class) => match cache::take(class) {
(Some(ptr), owner) => return (ptr, owner),
(None, owner) => (block(class), owner),
},
None => (layout, Owner::NONE),
};
let ptr = unsafe { std::alloc::alloc(layout) };
let ptr = NonNull::new(ptr).unwrap_or_else(|| std::alloc::handle_alloc_error(layout));
(ptr, owner)
}
pub(crate) unsafe fn free(ptr: NonNull<u8>, layout: Layout, owner: Owner) {
match class_of(layout) {
Some(class) => {
if !cache::keep(class, ptr, owner) {
unsafe { std::alloc::dealloc(ptr.as_ptr(), block(class)) };
}
}
None => unsafe { std::alloc::dealloc(ptr.as_ptr(), layout) },
}
}
#[cfg(all(test, not(loom)))]
mod tests {
use super::*;
const LAYOUT: Layout = match Layout::from_size_align(200, 16) {
Ok(layout) => layout,
Err(_) => panic!(),
};
struct Sent(NonNull<u8>, Owner);
unsafe impl Send for Sent {}
#[test]
fn classes_cover_their_sizes_and_alignments() {
for size in 1..=STEP * CLASSES {
let layout = Layout::from_size_align(size, 8).unwrap();
let class = class_of(layout).unwrap();
assert!(block(class).size() >= size);
assert!(block(class).size() - size < STEP);
}
assert_eq!(
class_of(Layout::from_size_align(STEP * CLASSES + 1, 8).unwrap()),
None
);
assert_eq!(class_of(Layout::from_size_align(64, 128).unwrap()), None);
}
#[test]
fn a_freed_block_is_the_next_one_of_its_class() {
let (a, owner) = alloc(LAYOUT);
unsafe { free(a, LAYOUT, owner) };
let other = Layout::from_size_align(250, 8).unwrap();
let (b, owner) = alloc(other);
assert_eq!(a, b, "the same class, the same block");
unsafe { free(b, other, owner) };
let big = Layout::from_size_align(STEP * CLASSES * 2, 8).unwrap();
let (c, owner) = alloc(big);
unsafe { free(c, big, owner) };
}
#[test]
fn a_block_freed_on_another_thread_goes_back_to_its_owner() {
std::thread::spawn(|| {
let (a, owner) = alloc(LAYOUT);
let sent = Sent(a, owner);
std::thread::spawn(move || {
let sent = sent;
unsafe { free(sent.0, LAYOUT, sent.1) };
})
.join()
.unwrap();
let (b, owner) = alloc(LAYOUT);
assert_eq!(a, b, "the block came back");
unsafe { free(b, LAYOUT, owner) };
})
.join()
.unwrap();
}
#[test]
fn a_block_its_owner_refuses_stays_with_the_thread_that_freed_it() {
std::thread::spawn(|| {
let (a, _) = alloc(LAYOUT);
let closed = Owner(&cache::CLOSED_RECORD);
unsafe { free(a, LAYOUT, closed) };
let (b, owner) = alloc(LAYOUT);
assert_eq!(a, b, "kept here");
unsafe { free(b, LAYOUT, owner) };
})
.join()
.unwrap();
}
#[test]
fn an_owners_list_takes_at_most_its_bound() {
std::thread::spawn(|| {
let bound = KEPT_BYTES / block(class_of(LAYOUT).unwrap()).size();
let blocks: Vec<_> = (0..=bound)
.map(|_| {
let (ptr, owner) = alloc(LAYOUT);
Sent(ptr, owner)
})
.collect();
let addrs: Vec<_> = blocks.iter().map(|b| b.0).collect();
let last = std::thread::spawn(move || {
let mut last = None;
for sent in blocks {
unsafe { free(sent.0, LAYOUT, sent.1) };
last = Some(sent.0);
}
let (b, owner) = alloc(LAYOUT);
assert_eq!(Some(b), last);
unsafe { free(b, LAYOUT, owner) };
});
last.join().unwrap();
let back: Vec<_> = (0..bound).map(|_| alloc(LAYOUT)).collect();
for (b, _) in &back {
assert!(addrs[..bound].contains(b), "a block given back");
}
for (b, owner) in back {
unsafe { free(b, LAYOUT, owner) };
}
})
.join()
.unwrap();
}
}