//! 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,,,,,, //! //! 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 { 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: Arc, push: fn(&Q, Pid), pop: fn(&Q) -> Option, 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, push: fn(&Q, Pid), pop: fn(&Q) -> Option, 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 = (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 ); } }