use core::ptr::NonNull;
use crate::allocator::Allocator;
use crate::central_pool::CentralPool;
use crate::config::AllocatorConfig;
use crate::free_list::FreeList;
use crate::size_class::{NUM_CLASSES, SizeClass};
pub struct ThreadCache {
lists: [FreeList; NUM_CLASSES],
config: AllocatorConfig,
owner: Option<usize>,
}
impl ThreadCache {
#[must_use]
pub fn new(config: AllocatorConfig) -> Self {
Self {
lists: core::array::from_fn(|_| FreeList::new()),
config,
owner: None,
}
}
pub(crate) fn bind_to_allocator(&mut self, allocator: &Allocator) {
let owner = allocator.id();
if self.owner == Some(owner) {
return;
}
assert!(
self.is_empty(),
"thread cache cannot be reused across allocators while it still holds cached blocks"
);
self.config = *allocator.config();
self.owner = Some(owner);
}
#[must_use]
fn is_empty(&self) -> bool {
self.lists.iter().all(FreeList::is_empty)
}
#[must_use]
pub(crate) fn pop(&mut self, class: SizeClass) -> Option<NonNull<u8>> {
unsafe { self.lists[class.index()].pop_block() }
}
pub(crate) unsafe fn push(&mut self, class: SizeClass, block: NonNull<u8>) {
unsafe {
self.lists[class.index()].push_block(block);
}
}
#[must_use]
pub(crate) const fn needs_refill(&self, class: SizeClass) -> bool {
self.lists[class.index()].is_empty()
}
pub(crate) fn refill_from_central(&mut self, class: SizeClass, central: &CentralPool) -> usize {
let batch = central.take_batch(class, self.config.refill_count(class));
let moved = batch.len();
unsafe {
self.lists[class.index()].push_batch(batch);
}
moved
}
#[must_use]
pub(crate) const fn should_drain(&self, class: SizeClass) -> bool {
self.lists[class.index()].len() > self.config.local_limit(class)
}
pub(crate) unsafe fn drain_excess_to_central(
&mut self,
class: SizeClass,
central: &CentralPool,
) -> usize {
if !self.should_drain(class) {
return 0;
}
let batch = {
let list = &mut self.lists[class.index()];
unsafe { list.pop_batch(self.config.drain_count(class)) }
};
let moved = batch.len();
unsafe {
central.return_batch(class, batch);
}
moved
}
#[allow(dead_code)]
pub(crate) unsafe fn drain_all_to_central(&mut self, central: &CentralPool) {
for class in SizeClass::ALL {
let batch = {
let list = &mut self.lists[class.index()];
unsafe { list.pop_batch(list.len()) }
};
unsafe {
central.return_batch(class, batch);
}
}
}
}
pub(crate) struct ThreadCacheHandle {
cache: ThreadCache,
owner: &'static Allocator,
}
impl ThreadCacheHandle {
#[must_use]
pub(crate) fn new(owner: &'static Allocator) -> Self {
Self {
cache: ThreadCache::new(*owner.config()),
owner,
}
}
pub(crate) fn with_parts<R>(&mut self, f: impl FnOnce(&Allocator, &mut ThreadCache) -> R) -> R {
f(self.owner, &mut self.cache)
}
}
impl Drop for ThreadCacheHandle {
fn drop(&mut self) {
self.owner.drain_thread_cache_on_exit(&mut self.cache);
}
}
#[cfg(test)]
mod tests {
use super::{ThreadCache, ThreadCacheHandle};
use crate::allocator::Allocator;
use crate::central_pool::CentralPool;
use crate::config::AllocatorConfig;
use crate::size_class::SizeClass;
use core::ptr::NonNull;
#[repr(align(64))]
struct TestBlock<const N: usize>([u8; N]);
impl<const N: usize> TestBlock<N> {
const fn new() -> Self {
Self([0; N])
}
fn as_ptr(&mut self) -> NonNull<u8> {
NonNull::from(&mut self.0).cast()
}
}
const fn test_config() -> AllocatorConfig {
AllocatorConfig {
arena_size: 512,
alignment: 64,
refill_target_bytes: 256,
local_cache_target_bytes: 384,
}
}
fn test_allocator() -> &'static Allocator {
let allocator = match Allocator::new(test_config()) {
Ok(allocator) => allocator,
Err(error) => panic!("expected allocator to initialize: {error}"),
};
Box::leak(Box::new(allocator))
}
fn fill_central<const N: usize>(
pool: &CentralPool,
class: SizeClass,
blocks: &mut [TestBlock<N>],
) {
let mut list = crate::free_list::FreeList::new();
for ptr in blocks.iter_mut().map(TestBlock::as_ptr) {
unsafe {
list.push_block(ptr);
}
}
let batch = unsafe { list.pop_batch(blocks.len()) };
unsafe {
pool.return_batch(class, batch);
}
}
#[test]
fn empty_cache_needs_refill_and_refill_moves_configured_batch() {
let pool = CentralPool::new();
let mut cache = ThreadCache::new(test_config());
let mut blocks = [
TestBlock::<{ SizeClass::B64.block_size() }>::new(),
TestBlock::<{ SizeClass::B64.block_size() }>::new(),
TestBlock::<{ SizeClass::B64.block_size() }>::new(),
];
fill_central(&pool, SizeClass::B64, &mut blocks);
assert!(cache.needs_refill(SizeClass::B64));
let moved = cache.refill_from_central(SizeClass::B64, &pool);
assert_eq!(moved, 2);
assert!(!cache.needs_refill(SizeClass::B64));
let remaining = pool.take_batch(SizeClass::B64, 8);
assert_eq!(remaining.len(), 1);
}
#[test]
fn should_drain_only_after_exceeding_local_limit() {
let mut cache = ThreadCache::new(test_config());
let mut blocks = [
TestBlock::<{ SizeClass::B64.block_size() }>::new(),
TestBlock::<{ SizeClass::B64.block_size() }>::new(),
TestBlock::<{ SizeClass::B64.block_size() }>::new(),
TestBlock::<{ SizeClass::B64.block_size() }>::new(),
];
for block in &mut blocks[..3] {
unsafe {
cache.push(SizeClass::B64, block.as_ptr());
}
}
assert!(!cache.should_drain(SizeClass::B64));
unsafe {
cache.push(SizeClass::B64, blocks[3].as_ptr());
}
assert!(cache.should_drain(SizeClass::B64));
}
#[test]
fn drain_excess_returns_exact_drain_batch_to_central() {
let pool = CentralPool::new();
let mut cache = ThreadCache::new(test_config());
let mut blocks = [
TestBlock::<{ SizeClass::B64.block_size() }>::new(),
TestBlock::<{ SizeClass::B64.block_size() }>::new(),
TestBlock::<{ SizeClass::B64.block_size() }>::new(),
TestBlock::<{ SizeClass::B64.block_size() }>::new(),
];
for block in &mut blocks {
unsafe {
cache.push(SizeClass::B64, block.as_ptr());
}
}
let moved = unsafe { cache.drain_excess_to_central(SizeClass::B64, &pool) };
assert_eq!(moved, 1);
let drained = pool.take_batch(SizeClass::B64, 8);
assert_eq!(drained.len(), 1);
let mut popped = 0;
while cache.pop(SizeClass::B64).is_some() {
popped += 1;
}
assert_eq!(popped, 3);
}
#[test]
fn drain_excess_is_a_noop_below_limit() {
let pool = CentralPool::new();
let mut cache = ThreadCache::new(test_config());
let mut blocks = [
TestBlock::<{ SizeClass::B64.block_size() }>::new(),
TestBlock::<{ SizeClass::B64.block_size() }>::new(),
TestBlock::<{ SizeClass::B64.block_size() }>::new(),
];
for block in &mut blocks {
unsafe {
cache.push(SizeClass::B64, block.as_ptr());
}
}
let moved = unsafe { cache.drain_excess_to_central(SizeClass::B64, &pool) };
assert_eq!(moved, 0);
assert_eq!(pool.take_batch(SizeClass::B64, 8).len(), 0);
}
#[test]
fn drain_all_moves_every_class_back_to_central() {
let pool = CentralPool::new();
let mut cache = ThreadCache::new(test_config());
let mut small = [
TestBlock::<{ SizeClass::B64.block_size() }>::new(),
TestBlock::<{ SizeClass::B64.block_size() }>::new(),
];
let mut medium = [TestBlock::<{ SizeClass::B256.block_size() }>::new()];
for block in &mut small {
unsafe {
cache.push(SizeClass::B64, block.as_ptr());
}
}
unsafe {
cache.push(SizeClass::B256, medium[0].as_ptr());
}
unsafe {
cache.drain_all_to_central(&pool);
}
assert!(cache.needs_refill(SizeClass::B64));
assert!(cache.needs_refill(SizeClass::B256));
assert_eq!(pool.take_batch(SizeClass::B64, 8).len(), 2);
assert_eq!(pool.take_batch(SizeClass::B256, 8).len(), 1);
}
#[test]
fn blocks_drained_from_one_cache_can_refill_another() {
let pool = CentralPool::new();
let mut source = ThreadCache::new(test_config());
let mut destination = ThreadCache::new(test_config());
let mut blocks = [
TestBlock::<{ SizeClass::B64.block_size() }>::new(),
TestBlock::<{ SizeClass::B64.block_size() }>::new(),
];
let expected = blocks.each_mut().map(TestBlock::as_ptr);
for block in &mut blocks {
unsafe {
source.push(SizeClass::B64, block.as_ptr());
}
}
unsafe {
source.drain_all_to_central(&pool);
}
let moved = destination.refill_from_central(SizeClass::B64, &pool);
assert_eq!(moved, 2);
assert_eq!(destination.pop(SizeClass::B64), Some(expected[1]));
assert_eq!(destination.pop(SizeClass::B64), Some(expected[0]));
assert_eq!(destination.pop(SizeClass::B64), None);
}
#[test]
fn thread_cache_handle_drop_drains_back_to_owner_central_pool() {
let allocator = test_allocator();
let mut blocks = [
TestBlock::<{ SizeClass::B64.block_size() }>::new(),
TestBlock::<{ SizeClass::B64.block_size() }>::new(),
];
{
let mut handle = ThreadCacheHandle::new(allocator);
handle.with_parts(|_, cache| {
for block in &mut blocks {
unsafe {
cache.push(SizeClass::B64, block.as_ptr());
}
}
});
}
let drained = allocator.take_central_batch_for_test(SizeClass::B64, 8);
assert_eq!(drained.len(), 2);
}
}