224 lines
7.6 KiB
Rust
224 lines
7.6 KiB
Rust
//! Raw run-queue microbench (ROADMAP_v0.5 phase 4).
|
||
//!
|
||
//! Benches the three queue STRUCTURES directly — no runtime, no actors — to
|
||
//! isolate the data structure under contention. All three types compile in
|
||
//! every build, so this binary covers the whole matrix in one run; it does
|
||
//! NOT need the rq-* feature rebuild dance (that's `rq_runtime`).
|
||
//!
|
||
//! Sweeps thread count × producer:consumer ratio. Queues are sized to the
|
||
//! item count, so the occupancy contract holds trivially and producers never
|
||
//! block on capacity.
|
||
//!
|
||
//! Knobs (env):
|
||
//! SMARM_BENCH_THREADS space-separated sweep, default "1 2 4"
|
||
//! SMARM_BENCH_ITEMS items per measurement, default 200_000
|
||
//! SMARM_BENCH_RUNS repetitions per config (median reported), default 5
|
||
//!
|
||
//! Output: the house table, plus one machine-readable line per config:
|
||
//! RQCSV,micro,<structure>,<threads>,<p:c>,<items>,<median_us>,<items_per_s>
|
||
//!
|
||
//! NOTE: numbers from a 1-core sandbox only validate the harness; real
|
||
//! contention curves come from the many-core box (scripts/bench_rq.sh).
|
||
|
||
use smarm::pid::Pid;
|
||
use smarm::run_queue::{MpmcRing, MutexQueue, StripedRing};
|
||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||
use std::sync::Arc;
|
||
use std::time::Instant;
|
||
|
||
fn env_usize(key: &str, default: usize) -> usize {
|
||
std::env::var(key)
|
||
.ok()
|
||
.and_then(|v| v.parse().ok())
|
||
.unwrap_or(default)
|
||
}
|
||
|
||
fn env_threads() -> Vec<usize> {
|
||
std::env::var("SMARM_BENCH_THREADS")
|
||
.map(|v| {
|
||
v.split_whitespace()
|
||
.filter_map(|t| t.parse().ok())
|
||
.collect()
|
||
})
|
||
.unwrap_or_else(|_| vec![1, 2, 4])
|
||
}
|
||
|
||
/// Generic driver: `producers` threads push `items` total, `consumers`
|
||
/// threads pop until everything is accounted for. Returns elapsed µs.
|
||
fn drive<Q: Send + Sync + 'static>(
|
||
q: Arc<Q>,
|
||
push: fn(&Q, Pid),
|
||
pop: fn(&Q) -> Option<Pid>,
|
||
producers: usize,
|
||
consumers: usize,
|
||
items: usize,
|
||
) -> u128 {
|
||
let remaining = Arc::new(AtomicUsize::new(items));
|
||
let start = Instant::now();
|
||
let mut hs = Vec::new();
|
||
let per = items / producers;
|
||
for p in 0..producers {
|
||
let q = q.clone();
|
||
// Give the last producer the remainder.
|
||
let n = if p == producers - 1 {
|
||
items - per * (producers - 1)
|
||
} else {
|
||
per
|
||
};
|
||
hs.push(std::thread::spawn(move || {
|
||
let pid = Pid::new(p as u32, 0);
|
||
for _ in 0..n {
|
||
push(&q, pid);
|
||
}
|
||
}));
|
||
}
|
||
for _ in 0..consumers {
|
||
let q = q.clone();
|
||
let remaining = remaining.clone();
|
||
hs.push(std::thread::spawn(move || loop {
|
||
// Claim-then-pop so consumers exit promptly when the budget hits
|
||
// zero; the claim is backed out on a miss.
|
||
let r = remaining.load(Ordering::Relaxed);
|
||
if r == 0 {
|
||
return;
|
||
}
|
||
if pop(&q).is_some() {
|
||
remaining.fetch_sub(1, Ordering::Relaxed);
|
||
} else {
|
||
std::hint::spin_loop();
|
||
}
|
||
}));
|
||
}
|
||
for h in hs {
|
||
h.join().unwrap();
|
||
}
|
||
start.elapsed().as_micros()
|
||
}
|
||
|
||
/// Single-thread alternating push/pop (the T = 1 case).
|
||
fn drive_single<Q>(q: &Q, push: fn(&Q, Pid), pop: fn(&Q) -> Option<Pid>, items: usize) -> u128 {
|
||
let pid = Pid::new(0, 0);
|
||
let start = Instant::now();
|
||
for _ in 0..items {
|
||
push(q, pid);
|
||
assert!(pop(q).is_some());
|
||
}
|
||
start.elapsed().as_micros()
|
||
}
|
||
|
||
struct Case {
|
||
structure: &'static str,
|
||
threads: usize,
|
||
producers: usize,
|
||
consumers: usize,
|
||
}
|
||
|
||
fn ratios_for(threads: usize) -> Vec<(usize, usize)> {
|
||
if threads < 2 {
|
||
return vec![(1, 1)]; // label only; T=1 runs the alternating driver
|
||
}
|
||
let mut v = vec![(threads / 2, threads - threads / 2)]; // balanced
|
||
if threads >= 4 {
|
||
v.push((3 * threads / 4, threads - 3 * threads / 4)); // producer-heavy
|
||
v.push((threads / 4, threads - threads / 4)); // consumer-heavy
|
||
}
|
||
v
|
||
}
|
||
|
||
fn main() {
|
||
let threads_sweep = env_threads();
|
||
let items = env_usize("SMARM_BENCH_ITEMS", 200_000);
|
||
let runs = env_usize("SMARM_BENCH_RUNS", 5);
|
||
|
||
println!("\n{}", "=".repeat(86));
|
||
println!(" run-queue raw structures — items={items}, runs={runs} (median)");
|
||
println!("{}", "=".repeat(86));
|
||
println!(
|
||
"{:>10} | {:>7} | {:>7} | {:>10} | {:>14}",
|
||
"structure", "threads", "p:c", "median µs", "items/s"
|
||
);
|
||
println!("{}", "-".repeat(86));
|
||
|
||
let mut cases = Vec::new();
|
||
for &t in &threads_sweep {
|
||
for (p, c) in ratios_for(t) {
|
||
for s in ["mutex", "mpmc", "striped"] {
|
||
cases.push(Case {
|
||
structure: s,
|
||
threads: t,
|
||
producers: p,
|
||
consumers: c,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
for case in cases {
|
||
let mut times: Vec<u128> = (0..runs)
|
||
.map(|_| {
|
||
// Fresh queue per run; capacity = items so pushes never stall.
|
||
match case.structure {
|
||
"mutex" => {
|
||
let q = Arc::new(MutexQueue::new(case.threads, items));
|
||
if case.threads < 2 {
|
||
drive_single(&*q, MutexQueue::push, MutexQueue::pop, items)
|
||
} else {
|
||
drive(
|
||
q,
|
||
MutexQueue::push,
|
||
MutexQueue::pop,
|
||
case.producers,
|
||
case.consumers,
|
||
items,
|
||
)
|
||
}
|
||
}
|
||
"mpmc" => {
|
||
let q = Arc::new(MpmcRing::with_capacity(items));
|
||
if case.threads < 2 {
|
||
drive_single(&*q, MpmcRing::push, MpmcRing::pop, items)
|
||
} else {
|
||
drive(
|
||
q,
|
||
MpmcRing::push,
|
||
MpmcRing::pop,
|
||
case.producers,
|
||
case.consumers,
|
||
items,
|
||
)
|
||
}
|
||
}
|
||
"striped" => {
|
||
let q = Arc::new(StripedRing::new(case.threads.max(1), items));
|
||
if case.threads < 2 {
|
||
drive_single(&*q, StripedRing::push, StripedRing::pop, items)
|
||
} else {
|
||
drive(
|
||
q,
|
||
StripedRing::push,
|
||
StripedRing::pop,
|
||
case.producers,
|
||
case.consumers,
|
||
items,
|
||
)
|
||
}
|
||
}
|
||
_ => unreachable!(),
|
||
}
|
||
})
|
||
.collect();
|
||
times.sort_unstable();
|
||
let median = times[times.len() / 2];
|
||
let per_s = (items as f64 / (median as f64 / 1e6)) as u64;
|
||
let ratio = format!("{}:{}", case.producers, case.consumers);
|
||
println!(
|
||
"{:>10} | {:>7} | {:>7} | {:>10} | {:>14}",
|
||
case.structure, case.threads, ratio, median, per_s
|
||
);
|
||
println!(
|
||
"RQCSV,micro,{},{},{},{},{},{}",
|
||
case.structure, case.threads, ratio, items, median, per_s
|
||
);
|
||
}
|
||
}
|