bench: CrossCalls lane, best-of sampling, 10ms sizes

This commit is contained in:
Oleksandr Kozachuk
2026-08-11 17:23:52 +02:00
parent 392f2d0136
commit 35da69cf7b
+91 -38
View File
@@ -731,7 +731,6 @@ struct PerfBenchmark {
run_code: &'static str, run_code: &'static str,
verify: &'static str, verify: &'static str,
expected: i32, expected: i32,
samples: u32, // Number of runs for WAFER median
/// Maximum acceptable WAFER/gforth ratio (< 1.0 = WAFER faster). /// Maximum acceptable WAFER/gforth ratio (< 1.0 = WAFER faster).
/// Test fails if ratio exceeds this. Set ~40-50% above measured baseline. /// Test fails if ratio exceeds this. Set ~40-50% above measured baseline.
max_ratio: f64, max_ratio: f64,
@@ -740,54 +739,67 @@ struct PerfBenchmark {
fn perf_benchmarks() -> Vec<PerfBenchmark> { fn perf_benchmarks() -> Vec<PerfBenchmark> {
vec![ vec![
PerfBenchmark { PerfBenchmark {
name: "Fibonacci(25)", name: "Fibonacci(33)",
define: ": FIB DUP 2 < IF EXIT THEN DUP 1- RECURSE SWAP 2 - RECURSE + ;", define: ": FIB DUP 2 < IF EXIT THEN DUP 1- RECURSE SWAP 2 - RECURSE + ;",
run_code: "25 FIB DROP", run_code: "33 FIB DROP",
verify: "25 FIB", verify: "33 FIB",
expected: 75025, expected: 3524578,
samples: 5,
max_ratio: 0.10, max_ratio: 0.10,
}, },
PerfBenchmark { PerfBenchmark {
name: "Factorial(12)x100K", name: "Factorial(12)x2M",
define: ": FACT 1 SWAP 1+ 1 ?DO I * LOOP ; \ define: ": FACT 1 SWAP 1+ 1 ?DO I * LOOP ; \
: FACT-BENCH 100000 0 DO 12 FACT DROP LOOP ;", : FACT-BENCH 2000000 0 DO 12 FACT DROP LOOP ;",
run_code: "FACT-BENCH", run_code: "FACT-BENCH",
verify: "12 FACT", verify: "12 FACT",
expected: 479001600, expected: 479001600,
samples: 5,
max_ratio: 0.12, max_ratio: 0.12,
}, },
PerfBenchmark { PerfBenchmark {
name: "GCD-bench(20K)", name: "GCD-bench(400K)",
define: ": GCD BEGIN DUP WHILE TUCK MOD REPEAT DROP ; \ define: ": GCD BEGIN DUP WHILE TUCK MOD REPEAT DROP ; \
: GCD-BENCH 0 DO 10000 I 1+ GCD DROP LOOP ;", : GCD-BENCH 0 DO 10000 I 1+ GCD DROP LOOP ;",
run_code: "20000 GCD-BENCH", run_code: "400000 GCD-BENCH",
verify: "48 36 GCD", verify: "48 36 GCD",
expected: 12, expected: 12,
samples: 5,
max_ratio: 0.45, max_ratio: 0.45,
}, },
PerfBenchmark { PerfBenchmark {
name: "NestedLoops(50)x1K", name: "NestedLoops(50)x20K",
define: ": NESTED 0 SWAP 0 DO I 0 ?DO I J + DROP LOOP LOOP ; \ define: ": NESTED 0 SWAP 0 DO I 0 ?DO I J + DROP LOOP LOOP ; \
: NESTED-BENCH 1000 0 DO 50 NESTED DROP LOOP ;", : NESTED-BENCH 20000 0 DO 50 NESTED DROP LOOP ;",
run_code: "NESTED-BENCH", run_code: "NESTED-BENCH",
verify: "5 NESTED", verify: "5 NESTED",
expected: 0, expected: 0,
samples: 5,
max_ratio: 0.11, max_ratio: 0.11,
}, },
PerfBenchmark { PerfBenchmark {
name: "Collatz(2K)", // The only benchmark with a cross-word call left in its hot loop:
// WORK is over the inliner's eight-operation budget, so it stays a
// real call. That is what CONSOLIDATE exists to turn into a direct
// one, and without this the CONSOL column measures nothing -- every
// other benchmark has its callee inlined away or self-recursive.
name: "CrossCalls(3M)",
define: ": WORK DUP 3 * OVER XOR SWAP 2 / XOR DUP 7 AND XOR DUP 1 AND XOR ; \
: CROSS-BENCH 0 SWAP 0 DO I WORK XOR LOOP ;",
run_code: "3000000 CROSS-BENCH DROP",
verify: "1000 CROSS-BENCH",
expected: 3176,
// Guards CONSOLIDATE as much as the engine: the ratio uses the
// better of the two columns, so a consolidation regression here
// pushes it from 0.04 to 0.12 and trips the limit.
max_ratio: 0.08,
},
PerfBenchmark {
name: "Collatz(2K)x50",
define: ": COLLATZ 0 SWAP BEGIN DUP 1 > WHILE \ define: ": COLLATZ 0 SWAP BEGIN DUP 1 > WHILE \
DUP 1 AND IF 3 * 1+ ELSE 2 / THEN \ DUP 1 AND IF 3 * 1+ ELSE 2 / THEN \
SWAP 1+ SWAP REPEAT DROP ; \ SWAP 1+ SWAP REPEAT DROP ; \
: COLLATZ-BENCH 0 DO I 1+ COLLATZ DROP LOOP ;", : COLLATZ-BENCH 0 DO I 1+ COLLATZ DROP LOOP ; \
run_code: "2000 COLLATZ-BENCH", : COLLATZ-REPEAT 50 0 DO 2000 COLLATZ-BENCH LOOP ;",
run_code: "COLLATZ-REPEAT",
verify: "27 COLLATZ", verify: "27 COLLATZ",
expected: 111, expected: 111,
samples: 3,
max_ratio: 0.08, max_ratio: 0.08,
}, },
] ]
@@ -833,15 +845,16 @@ fn measure_wafer_release(wafer: &str, bench: &PerfBenchmark) -> Option<u64> {
let code = format!( let code = format!(
"{define} {run} \ "{define} {run} \
: TIMED-BENCH UTIME {run} UTIME 2SWAP D- DROP . CR ; \ : TIMED-BENCH UTIME {run} UTIME 2SWAP D- DROP . CR ; \
TIMED-BENCH TIMED-BENCH TIMED-BENCH", {reps}",
define = bench.define, define = bench.define,
run = bench.run_code, run = bench.run_code,
reps = repeat_timed(" "),
); );
let output = run_via_stdin(wafer, &code)?; let output = run_via_stdin(wafer, &code)?;
if !output.status.success() { if !output.status.success() {
return None; return None;
} }
median_printed_time(&output.stdout) best_of_printed_times(&output.stdout)
} }
/// Measure WAFER execution time after CONSOLIDATE (direct calls between all words). /// Measure WAFER execution time after CONSOLIDATE (direct calls between all words).
@@ -849,31 +862,70 @@ fn measure_wafer_consolidated(wafer: &str, bench: &PerfBenchmark) -> Option<u64>
let code = format!( let code = format!(
"{define} CONSOLIDATE {run} \ "{define} CONSOLIDATE {run} \
: TIMED-BENCH UTIME {run} UTIME 2SWAP D- DROP . CR ; \ : TIMED-BENCH UTIME {run} UTIME 2SWAP D- DROP . CR ; \
TIMED-BENCH TIMED-BENCH TIMED-BENCH", {reps}",
define = bench.define, define = bench.define,
run = bench.run_code, run = bench.run_code,
reps = repeat_timed(" "),
); );
let output = run_via_stdin(wafer, &code)?; let output = run_via_stdin(wafer, &code)?;
if !output.status.success() { if !output.status.success() {
return None; return None;
} }
median_printed_time(&output.stdout) best_of_printed_times(&output.stdout)
} }
/// Parse the microsecond values printed by TIMED-BENCH (one per line) and /// How many separate process invocations each measurement takes the best of.
/// return the median. ///
fn median_printed_time(stdout: &[u8]) -> Option<u64> { /// `REPS`/`BEST_OF` deal with noise *inside* one process. They do not touch
/// the rest: whether a process lands on a core whose SMT sibling is busy, and
/// where its code ends up in memory, are fixed for its lifetime, and they make
/// some benchmarks frankly bimodal -- Fibonacci after CONSOLIDATE measured
/// 413-419 us in three runs of the report and 712-770 in the other two, with
/// nothing in between. Only a fresh process resamples that.
const PROCESS_RUNS: usize = 3;
/// How many timed repetitions each engine runs per benchmark.
const REPS: usize = 7;
/// How many of the fastest repetitions the reported time averages over.
const BEST_OF: usize = 3;
/// Run `measure` in `PROCESS_RUNS` fresh processes and keep the fastest.
///
/// The minimum, not a mean: process-level noise is one-sided too, so the
/// fastest process is the one that ran closest to undisturbed.
fn best_of_processes(mut measure: impl FnMut() -> Option<u64>) -> Option<u64> {
(0..PROCESS_RUNS).filter_map(|_| measure()).min()
}
/// `TIMED-BENCH` repeated `REPS` times, separated by `sep`.
///
/// sf64 needs one statement per line (it truncates input at ~256 characters);
/// the others do not care.
fn repeat_timed(sep: &str) -> String {
vec!["TIMED-BENCH"; REPS].join(sep)
}
/// Parse the microsecond values printed by `TIMED-BENCH` and reduce them to
/// one number: the mean of the fastest `BEST_OF`.
///
/// Not the median, and not the mean of all of them. Benchmark noise on a
/// shared machine is one-sided -- a scheduling hiccup, an SMT sibling or a
/// migration can only ever make a run slower, never faster -- so the fastest
/// repetitions are the ones closest to the cost we are trying to measure.
/// Averaging a few of them rather than taking the single minimum keeps one
/// lucky run from setting the result on its own.
fn best_of_printed_times(stdout: &[u8]) -> Option<u64> {
let stdout = String::from_utf8_lossy(stdout); let stdout = String::from_utf8_lossy(stdout);
let mut times: Vec<u64> = stdout let mut times: Vec<u64> = stdout
.trim() .trim()
.lines() .lines()
.filter_map(|l| l.trim().parse::<u64>().ok()) .filter_map(|l| l.trim().parse::<u64>().ok())
.collect(); .collect();
times.sort();
if times.is_empty() { if times.is_empty() {
return None; return None;
} }
Some(times[times.len() / 2]) times.sort_unstable();
let n = times.len().min(BEST_OF);
Some(times[..n].iter().sum::<u64>() / n as u64)
} }
/// Measure gforth execution time using Forth-level `utime` (excludes startup). /// Measure gforth execution time using Forth-level `utime` (excludes startup).
@@ -881,19 +933,19 @@ fn median_printed_time(stdout: &[u8]) -> Option<u64> {
/// Returns microseconds, or None if gforth is unavailable. /// Returns microseconds, or None if gforth is unavailable.
fn measure_gforth(gforth: &str, bench: &PerfBenchmark) -> Option<u64> { fn measure_gforth(gforth: &str, bench: &PerfBenchmark) -> Option<u64> {
// The timing wrapper must be inside a word (DO/LOOP is compile-only in gforth). // The timing wrapper must be inside a word (DO/LOOP is compile-only in gforth).
// We take the median of 3 runs.
let code = format!( let code = format!(
"{define} {run} \ "{define} {run} \
: TIMED-BENCH utime {run} utime 2swap d- drop . CR ; \ : TIMED-BENCH utime {run} utime 2swap d- drop . CR ; \
TIMED-BENCH TIMED-BENCH TIMED-BENCH bye", {reps} bye",
define = bench.define, define = bench.define,
run = bench.run_code, run = bench.run_code,
reps = repeat_timed(" "),
); );
let output = Command::new(gforth).arg("-e").arg(&code).output().ok()?; let output = Command::new(gforth).arg("-e").arg(&code).output().ok()?;
if !output.status.success() { if !output.status.success() {
return None; return None;
} }
median_printed_time(&output.stdout) best_of_printed_times(&output.stdout)
} }
/// Measure `SwiftForth` (`sf64`) execution time using Forth-level `ucounter` /// Measure `SwiftForth` (`sf64`) execution time using Forth-level `ucounter`
@@ -906,15 +958,16 @@ fn measure_sf64(sf64: &str, bench: &PerfBenchmark) -> Option<u64> {
let code = format!( let code = format!(
"{define}\n{run}\n\ "{define}\n{run}\n\
: TIMED-BENCH ucounter {run} ucounter 2swap d- drop . cr ;\n\ : TIMED-BENCH ucounter {run} ucounter 2swap d- drop . cr ;\n\
TIMED-BENCH\nTIMED-BENCH\nTIMED-BENCH\nbye\n", {reps}\nbye\n",
define = bench.define, define = bench.define,
run = bench.run_code, run = bench.run_code,
reps = repeat_timed("\n"),
); );
let output = run_via_stdin(sf64, &code)?; let output = run_via_stdin(sf64, &code)?;
if !output.status.success() { if !output.status.success() {
return None; return None;
} }
median_printed_time(&output.stdout) best_of_printed_times(&output.stdout)
} }
#[test] #[test]
@@ -987,14 +1040,14 @@ fn performance_report() {
for bench in &benchmarks { for bench in &benchmarks {
let wafer = wafer_release let wafer = wafer_release
.and_then(|w| measure_wafer_release(w, bench)) .and_then(|w| best_of_processes(|| measure_wafer_release(w, bench)))
.unwrap_or(0); .unwrap_or(0);
let consol = wafer_release let consol = wafer_release
.and_then(|w| measure_wafer_consolidated(w, bench)) .and_then(|w| best_of_processes(|| measure_wafer_consolidated(w, bench)))
.unwrap_or(0); .unwrap_or(0);
let gf = gforth.and_then(|g| measure_gforth(g, bench)); let gf = gforth.and_then(|g| best_of_processes(|| measure_gforth(g, bench)));
let gf_fast = gforth_fast.and_then(|g| measure_gforth(g, bench)); let gf_fast = gforth_fast.and_then(|g| best_of_processes(|| measure_gforth(g, bench)));
let sf = sf64.and_then(|s| measure_sf64(s, bench)); let sf = sf64.and_then(|s| best_of_processes(|| measure_sf64(s, bench)));
let gf_str = gf.map_or_else(|| "-".to_string(), |v| format!("{v}")); let gf_str = gf.map_or_else(|| "-".to_string(), |v| format!("{v}"));
let gf_fast_str = gf_fast.map_or_else(|| "-".to_string(), |v| format!("{v}")); let gf_fast_str = gf_fast.map_or_else(|| "-".to_string(), |v| format!("{v}"));