use std::cell::Cell;
use std::sync::atomic::{AtomicI64, AtomicU64, AtomicUsize, Ordering};
pub const STRIPES: usize = 8;
pub const CELLS: usize = STRIPES + 1;
#[repr(align(128))]
#[derive(Default)]
pub struct Padded<T>(pub T);
impl<T> std::ops::Deref for Padded<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
#[derive(Clone, Copy)]
pub struct Slot {
pub index: usize,
pub exclusive: bool,
}
static OWNED: AtomicUsize = AtomicUsize::new(0);
const UNSET: usize = usize::MAX;
const EXCLUSIVE: usize = 1 << 16;
struct Release(Cell<usize>);
impl Drop for Release {
fn drop(&mut self) {
let v = self.0.get();
if v != UNSET && v & EXCLUSIVE != 0 {
OWNED.fetch_and(!(1 << (v & !EXCLUSIVE)), Ordering::AcqRel);
}
}
}
thread_local! {
static SLOT: Release = const { Release(Cell::new(UNSET)) };
}
#[cold]
fn assign(cell: &Cell<usize>) -> usize {
let mut owned = OWNED.load(Ordering::Acquire);
loop {
let free = (!owned).trailing_zeros() as usize;
if free >= STRIPES {
cell.set(STRIPES);
return STRIPES;
}
match OWNED.compare_exchange_weak(owned, owned | (1 << free), Ordering::AcqRel, Ordering::Acquire) {
Ok(_) => {
cell.set(free | EXCLUSIVE);
return free | EXCLUSIVE;
}
Err(now) => owned = now,
}
}
}
#[inline]
pub fn slot() -> Slot {
let v = SLOT
.try_with(|s| {
let v = s.0.get();
if v != UNSET {
v
} else {
assign(&s.0)
}
})
.unwrap_or(STRIPES);
Slot { index: v & !EXCLUSIVE, exclusive: v & EXCLUSIVE != 0 }
}
#[inline]
pub fn bump(cell: &AtomicU64, n: u64, slot: Slot) {
if slot.exclusive {
cell.store(cell.load(Ordering::Relaxed).wrapping_add(n), Ordering::Relaxed);
} else {
cell.fetch_add(n, Ordering::Relaxed);
}
}
#[derive(Default)]
pub struct Counter {
cells: [Padded<AtomicU64>; CELLS],
base: AtomicU64,
}
impl Counter {
#[inline]
pub fn add(&self, n: u64) {
let s = slot();
bump(&self.cells[s.index], n, s);
}
fn sum(&self) -> u64 {
self.cells.iter().map(|c| c.load(Ordering::Relaxed)).fold(0u64, u64::wrapping_add)
}
pub fn get(&self) -> u64 {
self.sum().wrapping_sub(self.base.load(Ordering::Relaxed))
}
pub fn reset(&self) {
self.base.store(self.sum(), Ordering::Relaxed);
}
}
#[derive(Default)]
pub struct Gauge {
cells: [Padded<AtomicI64>; CELLS],
}
impl Gauge {
#[inline]
fn add(&self, n: i64) {
let s = slot();
let c = &self.cells[s.index];
if s.exclusive {
c.store(c.load(Ordering::Relaxed) + n, Ordering::SeqCst);
} else {
c.fetch_add(n, Ordering::SeqCst);
}
}
#[inline]
pub fn inc(&self) {
self.add(1);
}
#[inline]
pub fn dec(&self) {
self.add(-1);
}
pub fn get(&self) -> usize {
self.cells.iter().map(|c| c.load(Ordering::SeqCst)).sum::<i64>().max(0) as usize
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
#[test]
fn counters_sum_updates_from_many_threads() {
const THREADS: usize = 3 * STRIPES;
const N: u64 = 100_000;
let c = Arc::new(Counter::default());
let g = Arc::new(Gauge::default());
let start = Arc::new(std::sync::Barrier::new(THREADS));
let hs: Vec<_> = (0..THREADS)
.map(|_| {
let (c, g, start) = (c.clone(), g.clone(), start.clone());
std::thread::spawn(move || {
start.wait();
for _ in 0..N {
c.add(2);
g.inc();
}
for _ in 0..N / 4 {
g.dec();
}
})
})
.collect();
for h in hs {
h.join().unwrap();
}
assert_eq!(c.get(), THREADS as u64 * N * 2);
assert_eq!(g.get(), THREADS * (N - N / 4) as usize);
c.reset();
assert_eq!(c.get(), 0);
c.add(5);
assert_eq!(c.get(), 5);
assert!(slot().index < CELLS);
}
#[test]
fn stripes_are_given_back_when_their_threads_exit() {
for _ in 0..4 * STRIPES {
let (index, exclusive) = std::thread::spawn(|| {
let s = slot();
(s.index, s.exclusive)
})
.join()
.unwrap();
assert!(index < CELLS);
if exclusive {
let _ = OWNED.load(Ordering::Acquire);
}
}
let r = Release(Cell::new(UNSET));
drop(r); }
#[test]
fn a_gauge_decremented_on_another_stripe_does_not_go_negative() {
let g = Arc::new(Gauge::default());
let g2 = g.clone();
std::thread::spawn(move || g2.inc()).join().unwrap();
g.dec();
assert_eq!(g.get(), 0);
g.dec();
assert_eq!(g.get(), 0, "clamped at zero");
}
}