#![allow(unsafe_code)]
#![deny(missing_docs)]
#![allow(
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::semicolon_if_nothing_returned
)]
use std::alloc::{GlobalAlloc, Layout};
use std::hint::black_box;
use std::sync::mpsc::{channel, Receiver, Sender};
use std::sync::{Arc, Barrier};
use std::thread;
use std::time::Instant;
struct Block {
ptr: *mut u8,
layout: Layout,
}
unsafe impl Send for Block {}
struct XorShift64(u64);
impl XorShift64 {
fn new(seed: u64) -> Self {
Self(seed | 1)
}
#[inline]
fn next_u64(&mut self) -> u64 {
let mut x = self.0;
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
self.0 = x;
x.wrapping_mul(0x2545_F491_4F6C_DD1D)
}
#[inline]
fn below(&mut self, n: usize) -> usize {
(self.next_u64() % n as u64) as usize
}
}
#[inline]
fn pick_size(rng: &mut XorShift64) -> usize {
let r = rng.next_u64();
if r.is_multiple_of(32) {
512 + (r >> 8) as usize % (8 * 1024 - 512)
} else {
16 + (r >> 8) as usize % (512 - 16)
}
}
#[inline]
fn layout_for(size: usize) -> Layout {
Layout::from_size_align(size.max(1), 8).unwrap()
}
#[inline]
unsafe fn alloc_block<A: GlobalAlloc>(a: &A, rng: &mut XorShift64) -> Block {
let layout = layout_for(pick_size(rng));
let ptr = unsafe { a.alloc(layout) };
if !ptr.is_null() {
unsafe { ptr.write(0xA5) };
}
Block { ptr, layout }
}
#[inline]
unsafe fn free_block<A: GlobalAlloc>(a: &A, block: Block) {
if !block.ptr.is_null() {
unsafe { a.dealloc(block.ptr, block.layout) };
}
}
#[inline]
unsafe fn drain_mailbox<A: GlobalAlloc>(a: &A, rx: &Receiver<Block>, count: &mut u64) {
while let Ok(block) = rx.try_recv() {
unsafe { free_block(a, block) };
*count += 1;
}
}
unsafe fn larson_worker<A: GlobalAlloc>(
a: &A,
seed: u64,
steps: usize,
working_set: usize,
senders: &[Sender<Block>],
rx: &Receiver<Block>,
self_idx: usize,
) -> u64 {
let mut rng = XorShift64::new(seed);
let mut ops: u64 = 0;
let mut slots: Vec<Option<Block>> = Vec::with_capacity(working_set);
for _ in 0..working_set {
slots.push(Some(unsafe { alloc_block(a, &mut rng) }));
}
const HANDOFF_EVERY: usize = 16;
let n_threads = senders.len();
for step in 0..steps {
unsafe { drain_mailbox(a, rx, &mut ops) };
let idx = rng.below(working_set);
if n_threads > 1 && step % HANDOFF_EVERY == 0 {
if let Some(block) = slots[idx].take() {
let mut target = rng.below(n_threads);
if target == self_idx {
target = (target + 1) % n_threads;
}
if senders[target].send(block).is_err() {
}
}
} else if let Some(block) = slots[idx].take() {
unsafe { free_block(a, block) };
}
slots[idx] = Some(unsafe { alloc_block(a, &mut rng) });
ops += 1;
}
for block in slots.drain(..).flatten() {
unsafe { free_block(a, block) };
}
black_box(&ops);
ops
}
unsafe fn mstress_worker<A: GlobalAlloc>(
a: &A,
seed: u64,
rounds: usize,
block_count: usize,
senders: &[Sender<Block>],
rx: &Receiver<Block>,
self_idx: usize,
) -> u64 {
let mut rng = XorShift64::new(seed);
let mut ops: u64 = 0;
let n_threads = senders.len();
for _ in 0..rounds {
unsafe { drain_mailbox(a, rx, &mut ops) };
let mut blocks: Vec<Option<Block>> = Vec::with_capacity(block_count);
for _ in 0..block_count {
blocks.push(Some(unsafe { alloc_block(a, &mut rng) }));
}
let half = block_count / 2;
for _ in 0..half {
let idx = rng.below(block_count);
if let Some(block) = blocks[idx].take() {
if n_threads > 1 && rng.below(8) == 0 {
let mut target = rng.below(n_threads);
if target == self_idx {
target = (target + 1) % n_threads;
}
let _ = senders[target].send(block);
} else {
unsafe { free_block(a, block) };
}
ops += 1;
}
}
for slot in blocks.iter_mut() {
if slot.is_none() {
*slot = Some(unsafe { alloc_block(a, &mut rng) });
}
}
for block in blocks.drain(..).flatten() {
unsafe { free_block(a, block) };
ops += 1;
}
}
black_box(&ops);
ops
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Workload {
Larson,
Mstress,
}
#[derive(Clone, Debug)]
pub struct Config {
pub threads: usize,
pub steps_per_thread: usize,
pub working_set: usize,
pub mstress_blocks: usize,
}
impl Default for Config {
fn default() -> Self {
let threads = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(4);
Self {
threads,
steps_per_thread: 200_000,
working_set: 512,
mstress_blocks: 256,
}
}
}
pub fn run<A>(workload: Workload, config: &Config, make_alloc: fn() -> A) -> f64
where
A: GlobalAlloc + Send + 'static,
{
let threads = config.threads.max(1);
let steps = config.steps_per_thread;
let working_set = config.working_set.max(1);
let mstress_blocks = config.mstress_blocks.max(1);
let mut senders: Vec<Sender<Block>> = Vec::with_capacity(threads);
let mut receivers: Vec<Option<Receiver<Block>>> = Vec::with_capacity(threads);
for _ in 0..threads {
let (tx, rx) = channel::<Block>();
senders.push(tx);
receivers.push(Some(rx));
}
let senders = Arc::new(senders);
let barrier = Arc::new(Barrier::new(threads + 1));
let mut handles = Vec::with_capacity(threads);
for (t, rx_slot) in receivers.iter_mut().enumerate() {
let senders = Arc::clone(&senders);
let barrier = Arc::clone(&barrier);
let rx = rx_slot.take().unwrap();
let seed = 0x9E37_79B9_7F4A_7C15u64
.wrapping_mul(t as u64 + 1)
.wrapping_add(0xDEAD_BEEF);
let alloc = make_alloc();
let handle = thread::spawn(move || {
barrier.wait();
let ops = unsafe {
match workload {
Workload::Larson => {
larson_worker(&alloc, seed, steps, working_set, &senders, &rx, t)
}
Workload::Mstress => mstress_worker(
&alloc,
seed,
steps / mstress_blocks.max(1) + 1,
mstress_blocks,
&senders,
&rx,
t,
),
}
};
let mut extra = 0u64;
unsafe { drain_mailbox(&alloc, &rx, &mut extra) };
ops + extra
});
handles.push(handle);
}
barrier.wait();
let start = Instant::now();
let mut total_ops: u64 = 0;
for h in handles {
total_ops += h.join().expect("worker panicked");
}
let elapsed = start.elapsed();
drop(senders);
total_ops as f64 / elapsed.as_secs_f64()
}
pub fn sweep<A>(
workload: Workload,
config: &Config,
threads_sweep: &[usize],
make_alloc: fn() -> A,
) -> Vec<(usize, f64)>
where
A: GlobalAlloc + Send + 'static,
{
threads_sweep
.iter()
.map(|&t| {
let mut cfg = config.clone();
cfg.threads = t;
let ops = run(workload, &cfg, make_alloc);
(t, ops)
})
.collect()
}