#![allow(unsafe_code)]
use std::alloc::{GlobalAlloc, Layout, System};
use std::cell::{Cell, UnsafeCell};
use std::ptr;
use std::sync::atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering};
const SIZES: [usize; 16] = [
16, 32, 48, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 448, 512,
];
const CLASSES: usize = SIZES.len();
const MAX_SMALL: usize = 512;
const MAX_ALIGN: usize = 16;
const CHUNK: usize = 64 * 1024;
const BATCH: usize = 32;
const SHARDS: usize = 64;
const MAX_CPUS: usize = 4096;
const CLASS_OF: [u8; MAX_SMALL / 16 + 1] = {
let mut t = [0u8; MAX_SMALL / 16 + 1];
let mut i = 0;
while i < t.len() {
let size = if i == 0 { 1 } else { i * 16 };
let mut c = 0;
while SIZES[c] < size {
c += 1;
}
t[i] = c as u8;
i += 1;
}
t
};
#[inline]
fn class(layout: Layout) -> Option<usize> {
(layout.size() <= MAX_SMALL && layout.align() <= MAX_ALIGN)
.then(|| usize::from(CLASS_OF[layout.size().div_ceil(16)]))
}
struct Node {
next: *mut Node,
next_batch: *mut Node,
}
#[repr(align(128))]
struct Pool {
locked: AtomicBool,
batches: UnsafeCell<*mut Node>,
}
unsafe impl Sync for Pool {}
impl Pool {
const fn new() -> Self {
Pool {
locked: AtomicBool::new(false),
batches: UnsafeCell::new(ptr::null_mut()),
}
}
fn with<T>(&self, f: impl FnOnce(&mut *mut Node) -> T) -> T {
let mut spins = 0u32;
while self
.locked
.compare_exchange_weak(false, true, Ordering::Acquire, Ordering::Relaxed)
.is_err()
{
while self.locked.load(Ordering::Relaxed) {
spins += 1;
if spins < 64 {
std::hint::spin_loop();
} else {
std::thread::yield_now();
}
}
}
let r = f(unsafe { &mut *self.batches.get() });
self.locked.store(false, Ordering::Release);
r
}
unsafe fn push(&self, first: *mut Node) {
self.with(|batches| {
unsafe { (*first).next_batch = *batches };
*batches = first;
});
}
unsafe fn pop(&self) -> *mut Node {
self.with(|batches| {
let first = *batches;
if !first.is_null() {
*batches = unsafe { (*first).next_batch };
}
first
})
}
}
static POOLS: [[Pool; CLASSES]; SHARDS] = [const { [const { Pool::new() }; CLASSES] }; SHARDS];
static MULTI_NODE: AtomicBool = AtomicBool::new(false);
static CPU_SHARD: [AtomicU8; MAX_CPUS] = [const { AtomicU8::new(0) }; MAX_CPUS];
static BUDGET: AtomicUsize = AtomicUsize::new(CLASSES * CHUNK);
fn shard() -> &'static [Pool; CLASSES] {
&POOLS[current_shard()]
}
fn current_shard() -> usize {
if !MULTI_NODE.load(Ordering::Relaxed) {
return 0;
}
#[cfg(all(target_os = "linux", target_env = "gnu"))]
{
let cpu = unsafe { libc::sched_getcpu() };
if let Ok(cpu) = usize::try_from(cpu)
&& cpu < MAX_CPUS
{
return usize::from(CPU_SHARD[cpu].load(Ordering::Relaxed));
}
}
0
}
pub(crate) fn configure(cpu_node: &[(usize, usize)], l2_share: Option<usize>) {
let mut nodes = 0u64;
for &(cpu, node) in cpu_node {
let s = node % SHARDS;
nodes |= 1 << s;
if cpu < MAX_CPUS {
CPU_SHARD[cpu].store(s as u8, Ordering::Relaxed);
}
}
MULTI_NODE.store(nodes.count_ones() > 1, Ordering::Relaxed);
if let Some(b) = l2_share.filter(|&b| b > 0) {
BUDGET.store(b, Ordering::Relaxed);
}
}
#[doc(hidden)]
pub fn small_cache_simulate_numa(budget: usize) {
let cpus: Vec<(usize, usize)> = (0..MAX_CPUS).map(|c| (c, c)).collect();
configure(&cpus, Some(budget));
}
struct Cache {
head: [Cell<*mut Node>; CLASSES],
len: [Cell<usize>; CLASSES],
max: [Cell<usize>; CLASSES],
bump: [Cell<*mut u8>; CLASSES],
left: [Cell<usize>; CLASSES],
bytes: Cell<usize>,
}
thread_local! {
static CACHE: Cache = const {
Cache {
head: [const { Cell::new(ptr::null_mut()) }; CLASSES],
len: [const { Cell::new(0) }; CLASSES],
max: [const { Cell::new(BATCH) }; CLASSES],
bump: [const { Cell::new(ptr::null_mut()) }; CLASSES],
left: [const { Cell::new(0) }; CLASSES],
bytes: Cell::new(0),
}
};
}
impl Cache {
#[inline]
unsafe fn alloc(&self, c: usize) -> *mut u8 {
let head = self.head[c].get();
if !head.is_null() {
self.head[c].set(unsafe { (*head).next });
self.len[c].set(self.len[c].get() - 1);
self.bytes.set(self.bytes.get() - SIZES[c]);
return head.cast();
}
unsafe { self.refill(c) }
}
#[cold]
unsafe fn refill(&self, c: usize) -> *mut u8 {
let size = SIZES[c];
let left = self.left[c].get();
if left > 0 {
let p = self.bump[c].get();
self.left[c].set(left - 1);
self.bump[c].set(unsafe { p.add(size) });
return p;
}
self.max[c].set(self.max[c].get().saturating_add(BATCH));
let first = unsafe { shard()[c].pop() };
if !first.is_null() {
let rest = unsafe { (*first).next };
let mut n = 0;
let mut p = rest;
while !p.is_null() {
n += 1;
p = unsafe { (*p).next };
}
self.head[c].set(rest);
self.len[c].set(n);
self.bytes.set(self.bytes.get() + n * size);
return first.cast();
}
let chunk = unsafe { System.alloc(Layout::from_size_align_unchecked(CHUNK, 64)) };
if chunk.is_null() {
return ptr::null_mut();
}
self.bump[c].set(unsafe { chunk.add(size) });
self.left[c].set(CHUNK / size - 1);
chunk
}
#[inline]
unsafe fn free(&self, c: usize, p: *mut u8) {
let (len, bytes) = unsafe { self.free_quiet(c, p) };
if len > self.max[c].get() || bytes > BUDGET.load(Ordering::Relaxed) {
unsafe { self.spill(c) };
}
}
#[inline]
unsafe fn free_quiet(&self, c: usize, p: *mut u8) -> (usize, usize) {
let node = p.cast::<Node>();
unsafe { (&raw mut (*node).next).write(self.head[c].get()) };
self.head[c].set(node);
let len = self.len[c].get() + 1;
self.len[c].set(len);
let bytes = self.bytes.get() + SIZES[c];
self.bytes.set(bytes);
(len, bytes)
}
#[cold]
unsafe fn spill(&self, c: usize) {
let pools = shard();
if self.len[c].get() > self.max[c].get() {
unsafe { self.release(pools, c, BATCH) };
}
if self.bytes.get() > BUDGET.load(Ordering::Relaxed) {
for c in 0..CLASSES {
let n = self.len[c].get().div_ceil(2);
unsafe { self.release(pools, c, n) };
}
}
}
unsafe fn release(&self, pools: &[Pool; CLASSES], c: usize, mut n: usize) {
while n > 0 {
let mut head = self.head[c].get();
let (first, k) = unsafe { take(&mut head, n.min(BATCH)) };
if k == 0 {
return;
}
self.head[c].set(head);
self.len[c].set(self.len[c].get() - k);
self.bytes.set(self.bytes.get() - k * SIZES[c]);
unsafe { pools[c].push(first) };
n -= k;
}
}
}
impl Drop for Cache {
fn drop(&mut self) {
let pools = shard();
for (c, &size) in SIZES.iter().enumerate() {
for _ in 0..self.left[c].replace(0) {
let p = self.bump[c].get();
self.bump[c].set(unsafe { p.add(size) });
unsafe { self.free_quiet(c, p) };
}
let n = self.len[c].get();
unsafe { self.release(pools, c, n) };
}
}
}
unsafe fn take(head: &mut *mut Node, n: usize) -> (*mut Node, usize) {
let first = *head;
if first.is_null() {
return (first, 0);
}
let mut last = first;
let mut count = 1;
while count < n && !unsafe { (*last).next }.is_null() {
last = unsafe { (*last).next };
count += 1;
}
*head = unsafe { (*last).next };
unsafe { (*last).next = ptr::null_mut() };
(first, count)
}
unsafe fn global_alloc(c: usize) -> *mut u8 {
let pools = shard();
let first = unsafe { pools[c].pop() };
if !first.is_null() {
let rest = unsafe { (*first).next };
if !rest.is_null() {
unsafe { pools[c].push(rest) };
}
return first.cast();
}
unsafe { System.alloc(Layout::from_size_align_unchecked(SIZES[c], MAX_ALIGN)) }
}
pub struct SmallCache;
unsafe impl GlobalAlloc for SmallCache {
#[inline]
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
match class(layout) {
Some(c) => CACHE
.try_with(|k| unsafe { k.alloc(c) })
.unwrap_or_else(|_| unsafe { global_alloc(c) }),
None => unsafe { System.alloc(layout) },
}
}
#[inline]
unsafe fn dealloc(&self, p: *mut u8, layout: Layout) {
match class(layout) {
Some(c) => {
if CACHE.try_with(|k| unsafe { k.free(c, p) }).is_err() {
let node = p.cast::<Node>();
unsafe {
(&raw mut (*node).next).write(ptr::null_mut());
shard()[c].push(node);
}
}
}
None => unsafe { System.dealloc(p, layout) },
}
}
#[inline]
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
if class(layout).is_none() {
return unsafe { System.alloc_zeroed(layout) };
}
let p = unsafe { self.alloc(layout) };
if !p.is_null() {
unsafe { ptr::write_bytes(p, 0, layout.size()) };
}
p
}
#[inline]
unsafe fn realloc(&self, p: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
let new = unsafe { Layout::from_size_align_unchecked(new_size, layout.align()) };
match (class(layout), class(new)) {
(Some(a), Some(b)) if a == b => p,
(None, None) => unsafe { System.realloc(p, layout, new_size) },
_ => {
let q = unsafe { self.alloc(new) };
if !q.is_null() {
unsafe {
ptr::copy_nonoverlapping(p, q, layout.size().min(new_size));
self.dealloc(p, layout);
}
}
q
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_small_size_maps_to_the_smallest_class_that_fits() {
for size in 1..=MAX_SMALL {
let c = class(Layout::from_size_align(size, 8).unwrap()).unwrap();
assert!(SIZES[c] >= size, "{size}");
assert!(c == 0 || SIZES[c - 1] < size, "{size}");
}
assert_eq!(class(Layout::from_size_align(0, 1).unwrap()), Some(0));
assert_eq!(class(Layout::from_size_align(513, 8).unwrap()), None);
assert_eq!(class(Layout::from_size_align(64, 32).unwrap()), None);
}
#[test]
fn a_node_fits_in_the_smallest_block_and_pools_do_not_share_lines() {
assert!(std::mem::size_of::<Node>() <= SIZES[0]);
assert_eq!(std::mem::align_of::<Pool>(), 128);
}
}