[WIP] move towards lockless AtomTable

This commit is contained in:
Bennet Bleßmann
2023-08-28 23:24:27 +02:00
parent d24e6100a7
commit 01aeb7515d
17 changed files with 231 additions and 241 deletions

View File

@@ -7,16 +7,16 @@ use std::hash::{Hash, Hasher};
use std::mem;
use std::ops::Deref;
use std::ptr;
use std::ptr::NonNull;
use std::slice;
use std::str;
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::Weak;
use indexmap::IndexSet;
use modular_bitfield::prelude::*;
use tokio::runtime::Handle;
use tokio::sync::OwnedRwLockReadGuard;
use tokio::sync::RwLock;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
@@ -46,45 +46,35 @@ impl From<bool> for Atom {
}
}
impl indexmap::Equivalent<Atom> for LookupKey<'_, '_> {
impl indexmap::Equivalent<Atom> for str {
fn equivalent(&self, key: &Atom) -> bool {
&*key.as_str_with_table(self.0) == self.1
&*key.as_str() == self
}
}
const ATOM_TABLE_INIT_SIZE: usize = 1 << 16;
const ATOM_TABLE_ALIGN: usize = 8;
fn global_atom_table() -> &'static RwLock<Weak<RwLock<AtomTable>>> {
#[inline(always)]
fn global_atom_table() -> &'static RwLock<Weak<AtomTable>> {
#[cfg(feature = "rust_beta_channel")]
{
// const Weak::new will be stabilized in 1.73 which is currently in beta,
// till then we need a OnceLock for initialization
static GLOBAL_ATOM_TABLE: RwLock<Weak<RwLock<AtomTable>>> = RwLock::const_new(Weak::new());
static GLOBAL_ATOM_TABLE: RwLock<Weak<AtomTable>> = RwLock::const_new(Weak::new());
&GLOBAL_ATOM_TABLE
}
#[cfg(not(feature = "rust_beta_channel"))]
{
use std::sync::OnceLock;
static GLOBAL_ATOM_TABLE: OnceLock<RwLock<Weak<RwLock<AtomTable>>>> = OnceLock::new();
static GLOBAL_ATOM_TABLE: OnceLock<RwLock<Weak<AtomTable>>> = OnceLock::new();
GLOBAL_ATOM_TABLE.get_or_init(|| RwLock::new(Weak::new()))
}
}
fn owned_atom_table_read_guard() -> Option<OwnedRwLockReadGuard<AtomTable>> {
let atom_table = global_atom_table().blocking_read().upgrade()?;
let guard = {
// some test don't start a Runtime
if let Ok(handle) = Handle::try_current() {
handle.block_on(atom_table.read_owned())
} else {
tokio::runtime::Runtime::new()
.unwrap()
.block_on(atom_table.read_owned())
}
};
Some(guard)
#[inline(always)]
fn arc_atom_table() -> Option<Arc<AtomTable>> {
global_atom_table().blocking_read().upgrade()
}
impl RawBlockTraits for AtomTable {
@@ -132,7 +122,7 @@ macro_rules! is_char {
pub enum AtomString<'a> {
Static(&'a str),
Dynamic(OwnedRwLockReadGuard<AtomTable, str>),
Dynamic(AtomTableRef<str>),
}
impl AtomString<'_> {
@@ -142,7 +132,7 @@ impl AtomString<'_> {
{
match self {
Self::Static(reference) => Self::Static(f(reference)),
Self::Dynamic(guard) => Self::Dynamic(OwnedRwLockReadGuard::map(guard, f)),
Self::Dynamic(guard) => Self::Dynamic(AtomTableRef::map(guard, f)),
}
}
}
@@ -180,51 +170,28 @@ impl rustyline::completion::Candidate for AtomString<'_> {
}
impl Atom {
#[inline]
pub fn buf(self) -> Option<OwnedRwLockReadGuard<AtomTable, u8>> {
if let Some(guard) = self.as_ptr() {
Some(OwnedRwLockReadGuard::map(guard, |ptr| unsafe {
(ptr as *const u8)
.offset(mem::size_of::<AtomHeader>() as isize)
.as_ref()
.unwrap()
}))
} else {
None
}
}
#[inline(always)]
pub fn is_static(self) -> bool {
(self.index as usize) < STRINGS.len() << 3
}
#[inline(always)]
pub fn as_ptr_with_table<'at>(&self, atom_table: &'at AtomTable) -> Option<&'at u8> {
pub fn as_ptr(self) -> Option<AtomTableRef<u8>> {
if self.is_static() {
None
} else {
let atom_table =
arc_atom_table().expect("We should only have an Atom while there is an AtomTable");
unsafe {
atom_table
.buf()
.offset(((self.index as usize) - (STRINGS.len() << 3)) as isize)
.as_ref()
AtomTableRef::try_map(atom_table.buf(), |buf| {
(buf as *const u8)
.offset(((self.index as usize) - (STRINGS.len() << 3)) as isize)
.as_ref()
})
}
}
}
#[inline(always)]
pub fn as_ptr(self) -> Option<OwnedRwLockReadGuard<AtomTable, u8>> {
if self.is_static() {
None
} else {
let guard = owned_atom_table_read_guard()
.expect("We should only have an Atom while there is an AtomTable");
OwnedRwLockReadGuard::try_map(guard, |atom_table| self.as_ptr_with_table(atom_table))
.ok()
}
}
#[inline(always)]
pub fn from(index: u64) -> Self {
Self { index }
@@ -260,35 +227,29 @@ impl Atom {
}
}
#[inline(always)]
pub fn as_str_with_table<'at>(&self, atom_table: &'at AtomTable) -> &'at str {
if let Some(ptr) = self.as_ptr_with_table(atom_table) {
let header = unsafe { ptr::read::<AtomHeader>(ptr as *const u8 as *const AtomHeader) };
let len = header.len() as usize;
let buf = (unsafe { (ptr as *const u8).offset(mem::size_of::<AtomHeader>() as isize) })
as *mut u8;
unsafe { str::from_utf8_unchecked(slice::from_raw_parts(buf, len)) }
} else {
&STRINGS[(self.index >> 3) as usize]
}
}
#[track_caller]
#[inline]
pub fn as_str(&self) -> AtomString<'static> {
if self.is_static() {
AtomString::Static(STRINGS[(self.index >> 3) as usize])
} else {
let guard = owned_atom_table_read_guard()
.expect("We should only have an Atom while there is an AtomTable");
AtomString::Dynamic(OwnedRwLockReadGuard::map(guard, |atom_table| {
self.as_str_with_table(atom_table)
}))
if let Some(ptr) = self.as_ptr() {
AtomString::Dynamic(AtomTableRef::map(ptr, |ptr| {
let header =
unsafe { ptr::read::<AtomHeader>(ptr as *const u8 as *const AtomHeader) };
let len = header.len() as usize;
let buf = (unsafe {
(ptr as *const u8).offset(mem::size_of::<AtomHeader>() as isize)
}) as *mut u8;
unsafe { str::from_utf8_unchecked(slice::from_raw_parts(buf, len)) }
}))
} else {
AtomString::Static(&STRINGS[(self.index >> 3) as usize])
}
}
}
pub fn defrock_brackets(&self, atom_tbl: &Arc<RwLock<AtomTable>>) -> Self {
pub fn defrock_brackets(&self, atom_tbl: &AtomTable) -> Self {
let s = self.as_str();
let sub_str = if s.starts_with('(') && s.ends_with(')') {
@@ -297,9 +258,7 @@ impl Atom {
return *self;
};
let val = sub_str.to_string();
drop(s); // wee need to drop s as it holds a read lock on the AtomTable and build_with may need to acquire a write lock
AtomTable::build_with(&atom_tbl, &val)
AtomTable::build_with(&atom_tbl, &sub_str)
}
}
@@ -323,29 +282,80 @@ impl Ord for Atom {
}
}
pub struct AtomTableRef<M>
where
M: ?Sized,
{
arc: Arc<InnerAtomTable>,
data: NonNull<M>,
}
impl<M> Clone for AtomTableRef<M> {
fn clone(&self) -> Self {
Self {
arc: Arc::clone(&self.arc),
data: self.data,
}
}
}
impl<M: ?Sized> AtomTableRef<M> {
pub fn map<N: ?Sized, F: for<'a> FnOnce(&'a M) -> &'a N>(
referece: Self,
f: F,
) -> AtomTableRef<N> {
AtomTableRef {
arc: referece.arc,
data: f(unsafe { referece.data.as_ref() }).into(),
}
}
pub fn try_map<N, F: for<'a> FnOnce(&'a M) -> Option<&'a N>>(
referece: Self,
f: F,
) -> Option<AtomTableRef<N>> {
let val = f(unsafe { referece.data.as_ref() })?;
Some(AtomTableRef {
arc: Arc::clone(&referece.arc),
data: val.into(),
})
}
}
impl<M: ?Sized> Deref for AtomTableRef<M> {
type Target = M;
fn deref(&self) -> &Self::Target {
unsafe { self.data.as_ref() }
}
}
#[derive(Debug)]
pub struct AtomTable {
pub struct InnerAtomTable {
block: RawBlock<AtomTable>,
pub table: RwLock<IndexSet<Atom>>,
}
impl Drop for AtomTable {
fn drop(&mut self) {
self.block.deallocate();
}
#[derive(Debug)]
pub struct AtomTable {
inner: RwLock<Arc<InnerAtomTable>>,
// this lock is taking during resizing
update: Mutex<()>,
}
struct LookupKey<'table, 'key>(&'table AtomTable, &'key str);
impl Hash for LookupKey<'_, '_> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.1.hash(state);
impl InnerAtomTable {
#[inline(always)]
fn lookup_str(self: &InnerAtomTable, string: &str) -> Option<Atom> {
STATIC_ATOMS_MAP
.get(string)
.cloned()
.or_else(|| self.table.blocking_read().get(string).cloned())
}
}
impl AtomTable {
#[inline]
pub fn new() -> Arc<RwLock<Self>> {
pub fn new() -> Arc<Self> {
let upgraded = global_atom_table().blocking_read().upgrade();
// don't inline upgraded, otherwise temporary will be dropped too late in case of None
if let Some(atom_table) = upgraded {
@@ -356,84 +366,89 @@ impl AtomTable {
if let Some(atom_table) = guard.upgrade() {
atom_table
} else {
let atom_table = Arc::new(RwLock::new(Self {
block: RawBlock::new(),
table: RwLock::new(IndexSet::new()),
}));
let atom_table = Arc::new(Self {
inner: RwLock::new(Arc::new(InnerAtomTable {
block: RawBlock::new(),
table: RwLock::new(IndexSet::new()),
})),
update: Mutex::new(()),
});
*guard = Arc::downgrade(&atom_table);
atom_table
}
}
}
#[inline]
pub fn buf(&self) -> *const u8 {
self.block.base as *const u8
pub fn active_epoch(&self) -> AtomTableRef<InnerAtomTable> {
let arc = Arc::clone(&self.inner.blocking_read());
AtomTableRef {
data: arc.deref().into(),
arc,
}
}
#[inline]
pub fn top(&self) -> *const u8 {
self.block.top
}
#[inline(always)]
fn lookup_str(self: &AtomTable, string: &str) -> Option<Atom> {
STATIC_ATOMS_MAP.get(string).cloned().or_else(|| {
self.table
.blocking_read()
.get(&LookupKey(self, string))
.cloned()
pub fn buf(&self) -> AtomTableRef<u8> {
AtomTableRef::<InnerAtomTable>::map(self.active_epoch(), |inner| {
unsafe { inner.block.base.as_ref() }.unwrap()
})
}
pub fn build_with(atom_table: &RwLock<AtomTable>, string: &str) -> Atom {
let mut atom_table = loop {
// we can't just use blocking_write as tokio's RwLock is fair
// and we can't block readers here as otherwise we will deadlock, see the guard downgrade below
if let Ok(guard) = atom_table.try_write() {
break guard;
pub fn build_with(atom_table: &AtomTable, string: &str) -> Atom {
loop {
let mut epoch = atom_table.active_epoch();
let count = epoch.table.blocking_read().len();
if let Some(atom) = epoch.lookup_str(string) {
return atom;
}
};
if let Some(atom) = atom_table.lookup_str(string) {
return atom;
}
let update_guard = atom_table.update.lock().unwrap();
let is_same_allocation = Arc::ptr_eq(&epoch.arc, &atom_table.active_epoch().arc);
let is_same_atom_count = count == epoch.table.blocking_read().len();
if !(is_same_allocation && is_same_atom_count) {
// some other thread raced us between our lookup and us aquring the update lock, try again
continue;
}
unsafe {
let size = mem::size_of::<AtomHeader>() + string.len();
let align_offset = 8 * mem::align_of::<AtomHeader>();
let size = (size & !(align_offset - 1)) + align_offset;
let len_ptr = loop {
let ptr = atom_table.block.alloc(size);
unsafe {
let len_ptr = loop {
let ptr = epoch.block.alloc(size);
if ptr.is_null() {
atom_table.block.grow();
} else {
break ptr;
}
};
if ptr.is_null() {
let new_block = epoch.block.grow_new().unwrap();
let new_table = RwLock::new(epoch.table.blocking_read().clone());
let new_alloc = Arc::new(InnerAtomTable {
block: new_block,
table: new_table,
});
*atom_table.inner.blocking_write() = new_alloc;
epoch = atom_table.active_epoch();
} else {
break ptr;
}
};
let ptr_base = atom_table.block.base as usize;
let ptr_base = epoch.block.base as usize;
write_to_ptr(string, len_ptr);
write_to_ptr(string, len_ptr);
let atom = Atom {
index: ((STRINGS.len() << 3) + len_ptr as usize - ptr_base) as u64,
};
let atom = Atom {
index: ((STRINGS.len() << 3) + len_ptr as usize - ptr_base) as u64,
};
// we need to downgrade to a read so that Atom::hash can read from the AtomTable,
// so that it can calculate the hash for inserting the atom
// we can't just drop the guard as otherwise another thread could race us with another atom insertion
let atom_table = atom_table.downgrade();
epoch.table.blocking_write().insert(atom);
// NOTE: there is no race between downgrade and blocking write as table is only accessed writable in this function
// and only after the write lock is acquired as we have the guard and just convert it from a write to a read guard no writer can race us
atom_table.table.blocking_write().insert(atom);
drop(update_guard);
drop(atom_table); // we need to keep the guard around till after the insert
atom
return atom;
}
}
}
}