use crate::collector::collector_loop;
use crate::epoch::{AtomicEpoch, Color, Epoch, Phase};
use crate::guards::{Guard, Handle};
use crate::pointers::{ManObj, ManPtr, MarkObj, TraceObj};
use crate::sync::{Entry, Queue, ReusableSlots};
use crate::task::Task;
use crate::{global, pin};
use crossbeam::deque::{Stealer, Worker};
use crossbeam::epoch::{
Atomic as EbrAtomic, Guard as EbrGuard, Owned as EbrOwned, pin as ebr_pin, unprotected,
};
use crossbeam::utils::CachePadded;
use fastrand::Rng;
use rustc_hash::FxHashSet;
use std::array::from_fn;
use std::cell::{Cell, UnsafeCell};
use std::mem::{MaybeUninit, take};
use std::ops::DerefMut;
use std::ptr;
use std::rc::Rc;
use std::sync::atomic::{self, AtomicBool, AtomicPtr, AtomicUsize, Ordering, fence};
use std::thread::spawn;
pub(crate) const OBJ_BATCHES_SHARD: usize = 8;
pub(crate) const OBJ_BATCH_SIZE: usize = 64;
const HAZARDS_INIT_COUNT: usize = 8;
const ALLOC_HELPING_PERIOD: usize = 64;
const SCHED_HELPING_PERIOD: usize = 32;
pub struct Global {
pub(crate) locals: CachePadded<ReusableSlots<Local>>,
pub(crate) epoch: CachePadded<AtomicEpoch>,
pub(crate) stats: GlobalStats,
pub(crate) fresh_objs: [[Queue<ObjBatch>; OBJ_BATCHES_SHARD]; 2],
pub(crate) marked_objs: [[Queue<ObjBatch>; OBJ_BATCHES_SHARD]; 2],
collector_init: CachePadded<AtomicBool>,
pub(crate) collection_enabled: CachePadded<AtomicBool>,
pub(crate) collection_requested: CachePadded<AtomicBool>,
pub(crate) headroom: AtomicUsize,
pub(crate) collector_threads: AtomicUsize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HeapHeadroom {
FixedMiB(usize),
Proportional(usize),
}
const HEADROOM_PROPORTIONAL_BIT: usize = 1 << (usize::BITS - 1);
impl HeapHeadroom {
fn pack(self) -> usize {
match self {
Self::FixedMiB(mib) => mib.clamp(1, 1024) * 1024 * 1024,
Self::Proportional(div) => div.clamp(1, 1024) | HEADROOM_PROPORTIONAL_BIT,
}
}
fn unpack(bits: usize) -> Self {
if bits & HEADROOM_PROPORTIONAL_BIT != 0 {
Self::Proportional(bits & !HEADROOM_PROPORTIONAL_BIT)
} else {
Self::FixedMiB(bits / (1024 * 1024))
}
}
}
pub(crate) struct GlobalStats {
pub(crate) total_allocated: CachePadded<AtomicUsize>,
pub(crate) total_reclaimed: CachePadded<AtomicUsize>,
}
impl Default for GlobalStats {
fn default() -> Self {
Self {
total_allocated: CachePadded::new(AtomicUsize::new(0)),
total_reclaimed: CachePadded::new(AtomicUsize::new(0)),
}
}
}
unsafe impl Sync for Global {}
unsafe impl Send for Global {}
pub(crate) struct ObjBatch(Vec<Box<dyn MarkObj>>);
impl Default for ObjBatch {
fn default() -> Self {
Self::with_capacity(OBJ_BATCH_SIZE)
}
}
impl ObjBatch {
pub fn with_capacity(capacity: usize) -> Self {
Self(Vec::with_capacity(capacity))
}
pub fn push_within_capacity(&mut self, item: Box<dyn MarkObj>) -> Result<(), Box<dyn MarkObj>> {
if self.0.len() < self.0.capacity() {
self.0.push(item);
Ok(())
} else {
Err(item)
}
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &Box<dyn MarkObj>> {
self.0.iter()
}
pub fn into_iter(self) -> impl Iterator<Item = Box<dyn MarkObj>> {
self.0.into_iter()
}
}
impl Global {
#[inline]
pub(crate) fn new() -> Self {
Self {
locals: CachePadded::new(ReusableSlots::default()),
epoch: CachePadded::new(AtomicEpoch::new(Epoch::starting())),
stats: GlobalStats::default(),
fresh_objs: from_fn(|_| from_fn(|_| Queue::new())),
marked_objs: from_fn(|_| from_fn(|_| Queue::new())),
collector_init: CachePadded::new(AtomicBool::new(false)),
collection_enabled: CachePadded::new(AtomicBool::new(true)),
collection_requested: CachePadded::new(AtomicBool::new(false)),
headroom: AtomicUsize::new(HeapHeadroom::FixedMiB(1).pack()),
collector_threads: AtomicUsize::new(default_collector_threads()),
}
}
#[inline]
pub(crate) fn load_epoch(&self) -> Epoch {
self.epoch.load(Ordering::Acquire)
}
#[inline]
fn initialize_if_necessary(&self) {
if !self.collector_init.load(Ordering::Relaxed) {
self.try_deploy_collector();
}
}
#[cold]
fn try_deploy_collector(&self) {
if self
.collector_init
.compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed)
.is_ok()
{
spawn(collector_loop);
}
}
pub(crate) fn collect_hps(&self, ebr_guard: &EbrGuard) -> FxHashSet<*mut ()> {
self.locals
.iter_using()
.flat_map(|local| {
let hazards = local.hazards.load(Ordering::Acquire, ebr_guard);
unsafe { hazards.deref() }
.iter()
.map(|hp| unsafe { hp.assume_init_ref() }.load(Ordering::Relaxed))
})
.collect::<_>()
}
pub(crate) fn push_fresh_objs(
&self,
batch: ObjBatch,
size_bytes: usize,
alloc_color: Color,
shard_index: usize,
ebr_guard: &EbrGuard,
) {
unsafe {
self.fresh_objs
.get_unchecked(alloc_color as usize)
.get_unchecked(shard_index)
.push(batch, ebr_guard);
self.stats
.total_allocated
.fetch_add(size_bytes, Ordering::Release);
}
}
pub fn enable_collection(&self, set: bool) {
self.collection_enabled.store(set, Ordering::SeqCst);
}
pub fn request_collection(&self) {
self.collection_requested.store(true, Ordering::SeqCst);
}
pub fn estimate_total_alloc(&self) -> usize {
self.stats.total_allocated.load(Ordering::Acquire)
}
pub fn estimate_total_reclm(&self) -> usize {
self.stats.total_reclaimed.load(Ordering::Acquire)
}
pub fn estimate_heap_usage(&self) -> usize {
let allocated = self.estimate_total_alloc();
let reclaimed = self.estimate_total_reclm();
allocated.saturating_sub(reclaimed)
}
pub fn set_heap_headroom(&self, headroom: HeapHeadroom) {
self.headroom.store(headroom.pack(), Ordering::Relaxed);
}
pub fn heap_headroom(&self) -> HeapHeadroom {
HeapHeadroom::unpack(self.headroom.load(Ordering::Relaxed))
}
pub fn set_collector_threads(&self, count: usize) {
let clamped = count.clamp(1, OBJ_BATCHES_SHARD);
self.collector_threads.store(clamped, Ordering::Relaxed);
}
pub fn collector_threads(&self) -> usize {
self.collector_threads.load(Ordering::Relaxed)
}
}
fn default_collector_threads() -> usize {
let parallelism = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1);
(parallelism / 8).clamp(1, OBJ_BATCHES_SHARD)
}
pub(crate) struct Local {
guard_count: Cell<usize>,
last_observed: Cell<Epoch>,
alloc_count: Cell<usize>,
sched_count: Cell<usize>,
pub(crate) is_helping_normal: Cell<bool>,
pub(crate) is_helping_root_tracing: Cell<bool>,
pub(crate) is_helping_draining_mark_tasks: Cell<bool>,
pub(crate) hazards: EbrAtomic<[MaybeUninit<AtomicPtr<()>>]>,
#[allow(clippy::type_complexity)]
hazards_marker: UnsafeCell<Vec<Option<unsafe fn(*mut ())>>>,
pub(crate) mark_tasks_stealer: Stealer<Task>,
pub(crate) epoch: CachePadded<AtomicEpoch>,
pub(crate) mt_modified_ts: CachePadded<AtomicUsize>,
available_hids: UnsafeCell<Vec<usize>>,
rng: UnsafeCell<Rng>,
pub(crate) objs: [UnsafeCell<(ObjBatch, usize)>; 2],
pub(crate) mark_tasks: UnsafeCell<Worker<Task>>,
pub(crate) cached_hazards: UnsafeCell<Option<(FxHashSet<*mut ()>, Epoch)>>,
}
impl Drop for Local {
fn drop(&mut self) {
unsafe { take(&mut self.hazards).into_owned() };
}
}
impl Default for Local {
fn default() -> Self {
let mark_tasks = Worker::new_fifo();
let stealer = mark_tasks.stealer();
let mut hazards: EbrOwned<[MaybeUninit<AtomicPtr<()>>]> =
EbrOwned::init(HAZARDS_INIT_COUNT);
let slots = hazards.deref_mut();
unsafe {
ptr::write_bytes(slots.as_mut_ptr(), 0, slots.len());
}
Self {
guard_count: Cell::new(0),
last_observed: Cell::new(Epoch::starting()),
alloc_count: Cell::new(0),
sched_count: Cell::new(0),
is_helping_normal: Cell::new(false),
is_helping_root_tracing: Cell::new(false),
is_helping_draining_mark_tasks: Cell::new(false),
hazards: EbrAtomic::from(hazards),
hazards_marker: UnsafeCell::new(vec![None; HAZARDS_INIT_COUNT]),
mark_tasks_stealer: stealer,
epoch: CachePadded::new(AtomicEpoch::new(Epoch::starting())),
mt_modified_ts: CachePadded::new(AtomicUsize::new(0)),
available_hids: UnsafeCell::new((0..HAZARDS_INIT_COUNT).collect()),
rng: UnsafeCell::new(Rng::new()),
objs: [UnsafeCell::default(), UnsafeCell::default()],
mark_tasks: UnsafeCell::new(mark_tasks),
cached_hazards: UnsafeCell::default(),
}
}
}
impl Local {
pub(crate) fn register() -> Handle {
global().initialize_if_necessary();
let local = Rc::new(global().locals.acquire_or_default());
Handle { local }
}
#[inline]
pub(crate) fn is_pinned(&self) -> bool {
self.guard_count.get() > 0
}
#[inline]
pub(crate) fn pin_inner(&self) {
let guard_count = self.guard_count.get();
self.guard_count.set(guard_count.checked_add(1).unwrap());
if guard_count == 0 {
self.pin_freshly();
}
}
#[inline]
fn pin_freshly(&self) {
let mut curr_epoch = global().load_epoch();
loop {
self.epoch.store(curr_epoch.pinned(), Ordering::Release);
fence(Ordering::SeqCst);
let new_epoch = global().load_epoch();
if curr_epoch == new_epoch {
break;
}
curr_epoch = new_epoch;
}
if self.last_observed.get().timestamp() != curr_epoch.timestamp()
&& curr_epoch.phase() == Phase::RT
{
self.phase_barrier();
}
self.last_observed.set(curr_epoch);
}
#[inline]
pub(crate) fn unpin_inner(&self) {
let guard_count = self.guard_count.get();
self.guard_count.set(guard_count - 1);
if guard_count == 1 {
self.epoch.store(Epoch::starting(), Ordering::Release);
}
}
#[inline]
pub(crate) fn repin(&self) {
self.unpin_inner();
self.pin_inner();
}
#[inline]
pub(crate) fn phase_barrier(&self) {
let hazards = self
.hazards
.load(Ordering::Acquire, unsafe { unprotected() });
let hazards_marker = unsafe { &*self.hazards_marker.get() };
for (hp, mark) in unsafe { hazards.deref() }.iter().zip(hazards_marker.iter()) {
let ptr = unsafe { hp.assume_init_ref() }.load(Ordering::Relaxed);
if ptr.is_null() {
continue;
}
let mark = mark.unwrap();
unsafe { mark(ptr) };
}
}
#[inline]
pub(crate) fn acquire_hp(&self) -> usize {
let Some(hid) = (unsafe { (*self.available_hids.get()).pop() }) else {
self.grow_hazards();
return unsafe { (*self.available_hids.get()).pop() }.unwrap();
};
hid
}
#[cold]
pub(crate) fn grow_hazards(&self) {
let ebr_guard = &ebr_pin();
let old_sh = self.hazards.load(Ordering::Relaxed, ebr_guard);
let old_ref = unsafe { old_sh.deref() };
let mut new: EbrOwned<[MaybeUninit<AtomicPtr<()>>]> =
EbrOwned::init(old_ref.len().max(1) * 2);
let half = old_ref.len();
unsafe {
ptr::copy(old_ref.as_ptr(), new.deref_mut().as_mut_ptr(), half);
ptr::write_bytes(new.deref_mut().as_mut_ptr().add(half), 0, half);
}
self.hazards.store(new, Ordering::Release);
unsafe {
ebr_guard.defer_unchecked(move || old_sh.into_owned());
(*self.hazards_marker.get()).resize(old_ref.len() * 2, None);
(*self.available_hids.get()).extend(half..(half * 2));
}
}
#[inline]
pub(crate) fn release_hp(&self, hid: usize) {
unsafe { (*self.available_hids.get()).push(hid) };
}
#[inline]
pub(crate) unsafe fn pinned_alloc_color(&self) -> Color {
let epoch = unsafe { self.pinned_epoch() };
match epoch.phase() {
Phase::N => epoch.color(),
_ => epoch.color().flip(),
}
}
#[inline]
pub(crate) fn alloc<T: TraceObj>(&self, obj: ManObj<T>, guard: &Guard) -> *mut ManObj<T> {
let b = Box::new(obj);
let ptr = ((&*b) as *const ManObj<T>).cast_mut();
let b_dyn: Box<dyn MarkObj> = b;
unsafe { self.push_fresh_obj(b_dyn, true) };
let alloc_count = self.alloc_count.get() + 1;
self.alloc_count.set(alloc_count);
if alloc_count.is_multiple_of(ALLOC_HELPING_PERIOD) {
guard.schedule_helping_collect();
}
ptr
}
#[inline]
pub(crate) unsafe fn push_fresh_obj(&self, mut obj: Box<dyn MarkObj>, newly_allocated: bool) {
let obj_size = size_of_val(&*obj);
let objs_index = unsafe { self.pinned_alloc_color() } as usize;
loop {
let slot = unsafe { &mut *self.objs[objs_index].get() };
match slot.0.push_within_capacity(obj) {
Ok(_) => {
if newly_allocated {
slot.1 += obj_size;
}
break;
}
Err(e) => {
obj = e;
unsafe { self.flush_objs() };
}
}
}
}
#[inline]
pub(crate) fn select_obj_shard(&self) -> usize {
unsafe { &mut *self.rng.get() }.usize(0..OBJ_BATCHES_SHARD)
}
#[inline]
pub(crate) fn generate_shard_permut(&self) -> [usize; OBJ_BATCHES_SHARD] {
let mut result = [0, 1, 2, 3, 4, 5, 6, 7];
unsafe { &mut *self.rng.get() }.shuffle(&mut result);
result
}
#[inline]
pub(crate) unsafe fn take_obj_batch(&self, index: usize) -> Option<(ObjBatch, usize)> {
let batch_and_size = unsafe {
if (&*self.objs[index].get()).0.is_empty() {
return None;
}
ptr::replace(self.objs[index].get(), Default::default())
};
Some(batch_and_size)
}
#[inline]
pub(crate) unsafe fn flush_objs(&self) {
let alloc_color = unsafe { self.pinned_alloc_color() };
let index = alloc_color as usize;
let Some((batch, size_bytes)) = (unsafe { self.take_obj_batch(index) }) else {
return;
};
global().push_fresh_objs(
batch,
size_bytes,
alloc_color,
self.select_obj_shard(),
&ebr_pin(),
);
}
#[inline]
unsafe fn record_mt_modification(&self) {
let epoch = unsafe { self.pinned_epoch() };
let last_modified = unsafe { *(*self.mt_modified_ts).as_ptr() };
if last_modified == epoch.timestamp() {
return;
}
self.mt_modified_ts
.store(epoch.timestamp(), Ordering::Relaxed);
if unsafe { self.pinned_epoch() }.phase() == Phase::CT {
atomic::fence(Ordering::SeqCst); }
}
#[inline]
pub(crate) fn schedule_mark<T: TraceObj>(&self, obj: &ManObj<T>, guard: &Guard) {
let task = Task::new(|guard| obj.mark(guard));
let mark_task = unsafe {
self.record_mt_modification();
&*self.mark_tasks.get()
};
mark_task.push(task);
let sched_count = self.sched_count.get() + 1;
self.sched_count.set(sched_count);
if sched_count.is_multiple_of(SCHED_HELPING_PERIOD) {
guard.schedule_helping_collect();
}
}
#[inline]
pub(crate) unsafe fn pinned_epoch(&self) -> Epoch {
unsafe { self.epoch.load_non_atomic() }
}
#[inline]
pub(crate) fn try_pop_mark_task(&self) -> Option<Task> {
unsafe {
let tasks = &mut *self.mark_tasks.get();
if !tasks.is_empty() {
self.record_mt_modification();
if let Some(task) = tasks.pop() {
return Some(task);
}
}
}
None
}
#[inline]
pub(crate) fn scan_or_reuse_hazards<'g>(
&self,
guard: &'g Guard,
ebr_guard: &EbrGuard,
) -> &'g FxHashSet<*mut ()> {
unsafe {
let hazards = &mut *self.cached_hazards.get();
if let Some((hazards, prev_epoch)) = hazards
&& *prev_epoch == guard.local_epoch()
{
return hazards;
}
}
let new_hazards = global().collect_hps(ebr_guard);
unsafe {
let hazards = &mut *self.cached_hazards.get();
*hazards = Some((new_hazards, guard.local_epoch()));
&hazards.as_ref().unwrap_unchecked().0
}
}
}
pub struct HazardPointer {
hid: usize,
local: Rc<Entry<Local>>,
}
impl HazardPointer {
pub(crate) fn new(local: Rc<Entry<Local>>) -> Self {
let hid = local.acquire_hp();
Self { hid, local }
}
pub(crate) fn protect<T: TraceObj>(&self, addr: ManPtr<T>) {
unsafe fn mark<T: TraceObj>(ptr: *mut ()) {
let ptr = ManPtr::<T>::from(ptr);
unsafe { ptr.deref().mark(&pin()) };
}
self.hazard_slot()
.store(addr.as_ptr().cast(), Ordering::Release);
unsafe {
let local = self.local.as_ref();
let marker_ref = (&mut *local.hazards_marker.get()).get_unchecked_mut(self.hid);
*marker_ref = if addr.is_null() {
None
} else {
Some(mark::<T>)
};
}
}
pub(crate) fn clear(&self) {
self.hazard_slot().store(ptr::null_mut(), Ordering::Release);
unsafe {
let local = self.local.as_ref();
*(&mut *local.hazards_marker.get()).get_unchecked_mut(self.hid) = None;
}
}
fn hazard_slot(&self) -> &AtomicPtr<()> {
unsafe {
self.local
.as_ref()
.hazards
.load(Ordering::Relaxed, unprotected())
.deref()
.get_unchecked(self.hid)
.assume_init_ref()
}
}
}
impl Drop for HazardPointer {
fn drop(&mut self) {
self.clear();
self.local.as_ref().release_hp(self.hid);
}
}