ferroc 0.3.0

A fast & lock-free memory allocator library
Documentation
#![feature(allocator_api)]

use std::{
    alloc::Allocator,
    iter, mem,
    sync::Mutex,
    thread,
    time::{Duration, Instant},
};

use ferroc::Ferroc;

const COOKIE: usize = 0xbf58476d1ce4e5b9;
const THREADS: usize = 6;
const SCALE: usize = 50;
const ITER: usize = 25;
const TRANSFER_COUNT: usize = 1000;

#[cfg(feature = "track-valgrind")]
#[global_allocator]
static FERROC: Ferroc = Ferroc;

fn main() {
    println!("ferroc: {:?}", do_bench(&Ferroc));
    #[cfg(not(feature = "track-valgrind"))]
    println!("system: {:?}", do_bench(&std::alloc::System));
}

fn do_bench<A: Allocator + Send + Sync>(a: &A) -> Duration {
    let mut transfer: Vec<_> = iter::repeat_with(|| Mutex::new(None))
        .take(TRANSFER_COUNT)
        .collect();
    let start = Instant::now();
    for _ in 0..ITER {
        thread::scope(|s| {
            let transfer = &transfer;
            for tid in 0..THREADS {
                s.spawn(move || bench_one(tid, transfer, a));
            }
        });
        (transfer.iter_mut().filter(|_| probably(50)))
            .for_each(|t| drop(t.get_mut().unwrap().take()))
    }
    drop(transfer);
    start.elapsed()
}

fn bench_one<'a, A: Allocator>(tid: usize, transfer: &[Mutex<Option<Items<&'a A>>>], a: &'a A) {
    let mut alloc_count: usize = 100 * SCALE * (tid % 8 + 1);
    let mut retain_count: usize = alloc_count / 2;

    let mut retained = Vec::with_capacity_in(retain_count, a);
    let mut data = Vec::new_in(a);

    while alloc_count > 0 || retain_count > 0 {
        if retain_count == 0 || (probably(50) && alloc_count > 0) {
            data.push(Some(Items::new(1 << fastrand::u32(0..5), a)));
            alloc_count -= 1;
        } else {
            retained.push(Some(Items::new(1 << fastrand::u32(0..5), a)));
            retain_count -= 1;
        }

        if probably(67) && !data.is_empty() {
            let index = fastrand::usize(0..data.len());
            drop(data[index].take());
        }

        if probably(25) && !data.is_empty() {
            let di = fastrand::usize(0..data.len());
            let ti = fastrand::usize(0..transfer.len());
            mem::swap(&mut data[di], &mut transfer[ti].lock().unwrap());
        }
    }
}

#[inline]
fn probably(p: u8) -> bool {
    fastrand::u8(0..=100) <= p
}

struct Items<A: Allocator>(Box<[usize], A>);

impl<A: Allocator> Items<A> {
    fn new(count: usize, a: A) -> Self {
        let count = if probably(1) {
            if probably(1) {
                count * 10000
            } else if probably(10) {
                count * 1000
            } else {
                count * 100
            }
        } else {
            count
        };

        let mut vec = Vec::new_in(a);
        vec.extend((0..count).map(|i| (count - i) ^ COOKIE));
        Items(vec.into_boxed_slice())
    }
}

impl<A: Allocator> Drop for Items<A> {
    fn drop(&mut self) {
        for (index, &value) in self.0.iter().enumerate() {
            assert_eq!(
                value ^ COOKIE,
                (self.0.len() - index),
                "memory corruption at block {:p} at {index}",
                self.0
            )
        }
    }
}