use core::ops::Deref;
use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use mnemosyne_core::constants::NUM_SIZE_CLASSES;
use mnemosyne_core::policy::AllocPolicy;
use std::boxed::Box;
use std::sync::OnceLock;
const MAX_CACHED_BLOCKS: usize = 8;
const MAX_CPUS: usize = 256;
#[cfg(test)]
pub static PER_CPU_CACHE_ENABLED: AtomicBool = AtomicBool::new(false);
#[cfg(not(test))]
pub static PER_CPU_CACHE_ENABLED: AtomicBool = AtomicBool::new(true);
#[repr(align(64))]
pub struct CpuCacheSlot {
pub blocks: [[AtomicUsize; MAX_CACHED_BLOCKS]; NUM_SIZE_CLASSES],
}
impl Default for CpuCacheSlot {
#[inline]
fn default() -> Self {
Self::new()
}
}
impl CpuCacheSlot {
pub const fn new() -> Self {
Self {
blocks: [const { [const { AtomicUsize::new(0) }; MAX_CACHED_BLOCKS] };
NUM_SIZE_CLASSES],
}
}
}
#[repr(align(64))]
pub struct PerCpuCache {
pub slots: [CpuCacheSlot; MAX_CPUS],
}
impl Default for PerCpuCache {
#[inline]
fn default() -> Self {
Self::new()
}
}
impl PerCpuCache {
pub const fn new() -> Self {
Self {
slots: [const { CpuCacheSlot::new() }; MAX_CPUS],
}
}
}
pub struct PerCpuCacheHandle {
storage: OnceLock<Box<PerCpuCache>>,
}
impl PerCpuCacheHandle {
pub const fn new() -> Self {
Self {
storage: OnceLock::new(),
}
}
#[inline]
fn get(&self) -> &PerCpuCache {
self.storage
.get_or_init(|| Box::new(PerCpuCache::new()))
.as_ref()
}
}
impl Default for PerCpuCacheHandle {
fn default() -> Self {
Self::new()
}
}
impl Deref for PerCpuCacheHandle {
type Target = PerCpuCache;
#[inline]
fn deref(&self) -> &Self::Target {
self.get()
}
}
pub static PER_CPU_CACHE: PerCpuCacheHandle = PerCpuCacheHandle::new();
const _: () =
assert!(core::mem::size_of::<PerCpuCacheHandle>() < core::mem::size_of::<PerCpuCache>());
static DISABLE_CPU_CACHE: AtomicBool = AtomicBool::new(false);
pub fn disable_cpu_cache() {
DISABLE_CPU_CACHE.store(true, Ordering::Relaxed);
}
pub fn enable_cpu_cache() {
DISABLE_CPU_CACHE.store(false, Ordering::Relaxed);
}
#[inline]
pub fn current_cpu_id() -> usize {
themis::current_processor().map_or(0, |cpu| cpu as usize) % MAX_CPUS
}
melinoe::thread_cached! {
mod cached_cpu_id: usize;
}
#[inline(always)]
pub fn get_current_cpu_id() -> usize {
cached_cpu_id::get_or_init(current_cpu_id)
}
#[inline(always)]
pub fn refresh_current_cpu_id() -> usize {
let actual = current_cpu_id();
cached_cpu_id::set(actual);
actual
}
#[inline(always)]
pub fn try_alloc_cpu<P: AllocPolicy>(class: usize) -> *mut u8 {
if P::ENABLE_FREE_LIST_ENCRYPTION {
return core::ptr::null_mut();
}
if DISABLE_CPU_CACHE.load(Ordering::Relaxed) || !PER_CPU_CACHE_ENABLED.load(Ordering::Relaxed) {
return core::ptr::null_mut();
}
let mut cpu_id = get_current_cpu_id();
let mut slot = &PER_CPU_CACHE.slots[cpu_id];
let mut refreshed = false;
for _ in 0..2 {
let mut found_idx = None;
let mut block_ptr_val = 0;
for i in 0..MAX_CACHED_BLOCKS {
let val = slot.blocks[class][i].load(Ordering::Relaxed);
if val != 0 {
found_idx = Some(i);
block_ptr_val = val;
break;
}
}
let Some(idx) = found_idx else {
if !refreshed {
let new_cpu_id = refresh_current_cpu_id();
if new_cpu_id != cpu_id {
cpu_id = new_cpu_id;
slot = &PER_CPU_CACHE.slots[cpu_id];
refreshed = true;
continue;
}
}
return core::ptr::null_mut();
};
match slot.blocks[class][idx].compare_exchange_weak(
block_ptr_val,
0,
Ordering::Acquire,
Ordering::Relaxed,
) {
Ok(_) => {
return block_ptr_val as *mut u8;
}
Err(_) => {
if !refreshed {
let new_cpu_id = refresh_current_cpu_id();
if new_cpu_id != cpu_id {
cpu_id = new_cpu_id;
slot = &PER_CPU_CACHE.slots[cpu_id];
}
refreshed = true;
} else {
break;
}
}
}
}
core::ptr::null_mut()
}
#[inline(always)]
pub fn try_free_cpu(ptr: *mut u8, class: usize, encrypted: bool) -> bool {
if ptr.is_null() {
return false;
}
if encrypted {
return false;
}
if DISABLE_CPU_CACHE.load(Ordering::Relaxed) || !PER_CPU_CACHE_ENABLED.load(Ordering::Relaxed) {
return false;
}
let mut cpu_id = get_current_cpu_id();
let mut slot = &PER_CPU_CACHE.slots[cpu_id];
let mut refreshed = false;
for _ in 0..2 {
let mut found_idx = None;
let mut is_double_free = false;
for i in 0..MAX_CACHED_BLOCKS {
let val = slot.blocks[class][i].load(Ordering::Relaxed);
if val == ptr as usize {
is_double_free = true;
break;
}
if val == 0 && found_idx.is_none() {
found_idx = Some(i);
}
}
if is_double_free {
std::process::abort();
}
let Some(idx) = found_idx else {
if !refreshed {
let new_cpu_id = refresh_current_cpu_id();
if new_cpu_id != cpu_id {
cpu_id = new_cpu_id;
slot = &PER_CPU_CACHE.slots[cpu_id];
refreshed = true;
continue;
}
}
return false;
};
match slot.blocks[class][idx].compare_exchange_weak(
0,
ptr as usize,
Ordering::Release,
Ordering::Relaxed,
) {
Ok(_) => {
return true;
}
Err(_) => {
if !refreshed {
let new_cpu_id = refresh_current_cpu_id();
if new_cpu_id != cpu_id {
cpu_id = new_cpu_id;
slot = &PER_CPU_CACHE.slots[cpu_id];
}
refreshed = true;
} else {
break;
}
}
}
}
false
}
#[cfg(test)]
mod tests {
use super::{MAX_CPUS, PerCpuCacheHandle};
#[test]
fn cache_handle_allocates_storage_on_first_access() {
let handle = PerCpuCacheHandle::new();
assert_eq!(handle.storage.get().map(|cache| cache.slots.len()), None);
let _cache = handle.get();
assert_eq!(
handle.storage.get().map(|cache| cache.slots.len()),
Some(MAX_CPUS)
);
}
}