- benches/rq_micro.rs: raw-structure microbench, threads x producer:consumer ratio sweep. Benches all three queue types in one binary (they compile in every build; only the runtime alias is feature-selected), so no rebuild dance. Queues sized to the op count so the occupancy contract is met. - benches/rq_runtime.rs: whole-runtime benches with the selected variant: yield-storm (pure queue churn), ping-pong-pairs (park/unpark latency), spawn-storm (slab + free list + queue under churn). Scheduler-count sweep. - scripts/bench_rq.sh: rebuilds rq_runtime per rq-* feature, runs rq_micro once, aggregates RQCSV lines into bench_results/summary.csv. - All knobs via SMARM_BENCH_* env vars; house table format + machine lines. - run_queue module is now #[doc(hidden)] pub (types + push/pop/len + MpmcRing::with_capacity) solely so the external bench binary can drive the raw structures. docs(roadmap): phase 4 ticked (harness done; numbers from the 20-core box). New fast-follow per review: assert the invariants we lean on — debug_assert! on hot paths, loud assert!/panic on cold ones, at the point of reliance; sweep existing code during the phase-5 audit, adopt as house style. Validated end-to-end at smoke scale on the 1-core sandbox: full driver run, 24-row summary.csv across micro (3 structures x sweeps) and runtime (3 variants x 3 benches x thread sweep).
187 lines
6.7 KiB
Rust
187 lines
6.7 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
|
||
);
|
||
}
|
||
}
|