Benchmark results
Wall-clock means from criterion across four harnesses. Overhead
pits Fused against handrolled single-threaded recursions to measure
framework cost. Matrix runs Funnel across its 16 policy variants
alongside Rayon and a scoped pool, all parallel, on 14 workload
scenarios. Module simulation runs a synthetic dependency-graph
resolver. Quick is a development-iteration subset of the Matrix
grid.
Interactive: Funnel axes viewer
A per-cell view of the Matrix data, all values normalised to the
real.rayon baseline (positive = slower than rayon, negative =
faster). Three axis toggles fold groups of Funnel variants
together; per-workload toggles include or exclude each scenario
from the aggregated summaries. noop is excluded by default —
the cell takes sub-millisecond and percentage deltas distort
relative to scenarios with non-trivial work.
Toggle a scenario off if its shape is not representative of the
workload you care about; toggle a policy axis to marginalise it
and see how the other two co-vary. The panels below the table
recompute as the selection changes: per-axis mean deviation,
pairwise axis interaction effects, and per-workload best/worst
Funnel preset.
Matrix
Sixteen Funnel policy variants × 14 workload scenarios, plus
handrolled Rayon and a scoped-pool baseline. Each cell shows the
wall-clock mean and the +X% deviation from the row’s fastest
entry; the row winner is marked (best).
make -C hylic-benchmark bench-matrix
Module simulation
A synthetic dependency-graph resolver: eight workloads on two
axes, sparse vs dense graph and fast vs slow per-node work. The
_fast rows isolate scheduling cost; the _slow rows let
per-node work dominate so runners converge.
make -C hylic-benchmark bench-modsim
Overhead
Sequential framework cost: Fused against a handrolled
fn rec(n) -> R { … } over the same workloads, plus a few
parallel runners (real.rayon, hylic-rayon) for cross-reference.
hylic-rayon versus real.rayon is the apples-to-apples pair
for the parallel framework tax.
make -C hylic-benchmark bench-overhead
Quick
A subset for development iteration: five runners (real.rayon
plus four Funnel presets covering the queue × accumulation
axes), nine workloads chosen for variation. The -ab form runs
the same bench across multiple git revisions of hylic and
archives results with a timestamp.
make bench-quick-light
What the numbers say
Fused lands close to the handrolled sequential baselines —
within compiler-noise margins on the Overhead rows rather than
integer multiples — so the closure-based fold/treeish surface is
not paying for itself in single-threaded performance. On the
parallel side, no single Funnel preset wins across the Matrix:
shallow-wide workloads tend to favour shared queues with arrival
delivery, deep-narrow workloads tend to favour per-worker deques
with finalize-buffering, and the wake axis can move a row by
itself. The viewer above is the practical way to find the preset
for a particular workload shape; the row-by-row reading of the
Matrix table is the same information without the marginalisation
tools.
The Module-simulation harness picks at the same trade in a more
focused frame: the _fast rows show what scheduling decides when
per-node work is cheap (the dependency-resolver case); the
_slow rows show that scheduling stops mattering once the work
itself dominates.
The implementation that produces these properties — policies as
generic parameters on Funnel<P> so each preset compiles to its
own walk, defunctionalised continuations in scoped arenas, no
per-step heap traffic — is documented in the
Funnel deep-dive. The numbers above are
observations of the result, not derivations of the design.
Workload scenarios
Each scenario is a TreeSpec (node count, branching factor)
and a WorkSpec (per-phase CPU burn amounts plus an optional
I/O spin-wait). busy_work is a deterministic u64 LCG loop
inside black_box; spin_wait_us is a wall-clock busy-wait.
The scenarios cover a shape space — shallow-wide, deep-narrow,
accumulate-heavy, finalize-heavy, I/O-bound, graph-discovery —
not any specific production workload.
#![allow(unused)]
fn main() {
pub fn all_scenarios(scale: Scale) -> Vec<ScenarioDef> {
let (n, n_large) = match scale {
Scale::Small => (200, 500),
Scale::Large => (2000, 5000),
};
vec![
def("noop", "noop", TreeSpec { node_count: n, branch_factor: 8 }, w(0, 0, 0, 0, 0)),
def("hashtable", "hash", TreeSpec { node_count: n, branch_factor: 8 }, w(5_000, 1_000, 0, 5_000, 0)),
def("parse-light", "parse-lt", TreeSpec { node_count: n, branch_factor: 8 }, w(50_000, 5_000, 5_000, 10_000, 0)),
def("parse-heavy", "parse-hv", TreeSpec { node_count: n, branch_factor: 8 }, w(200_000, 10_000, 10_000, 50_000, 0)),
def("aggregate", "aggr", TreeSpec { node_count: n, branch_factor: 8 }, w(5_000, 100_000, 5_000, 5_000, 0)),
def("transform", "xform", TreeSpec { node_count: n, branch_factor: 8 }, w(5_000, 5_000, 100_000, 5_000, 0)),
def("finalize-only","fin", TreeSpec { node_count: n, branch_factor: 8 }, w(0, 0, 100_000, 0, 0)),
def("balanced", "bal", TreeSpec { node_count: n, branch_factor: 8 }, w(50_000, 50_000, 50_000, 50_000, 0)),
def("io-bound", "io", TreeSpec { node_count: n, branch_factor: 8 }, w(5_000, 0, 0, 0, 200)),
def("wide-shallow", "wide", TreeSpec { node_count: n, branch_factor: 20 }, w(50_000, 10_000, 10_000, 10_000, 0)),
def("deep-narrow", "deep", TreeSpec { node_count: n, branch_factor: 2 }, w(50_000, 10_000, 10_000, 10_000, 0)),
def("large-dense", "lg-dense", TreeSpec { node_count: n_large, branch_factor: 10 }, w(50_000, 10_000, 10_000, 10_000, 0)),
// Graph-heavy: edge discovery is the dominant cost (like module resolution).
// Light dict-merge accumulate. Models dependency graph traversal.
def("graph-heavy", "graph-hv", TreeSpec { node_count: n, branch_factor: 8 }, w(5_000, 10_000, 5_000, 200_000, 0)),
// Graph-heavy with I/O: each edge has latency (like filesystem lookup).
def("graph-io", "graph-io", TreeSpec { node_count: n, branch_factor: 8 }, w(5_000, 10_000, 5_000, 50_000, 200)),
]
}
}
#![allow(unused)]
fn main() {
/// Per-phase work distribution.
#[derive(Clone)]
pub struct WorkSpec {
pub init_work: u64,
pub accumulate_work: u64,
pub finalize_work: u64,
pub graph_work: u64,
pub graph_io_us: u64,
}
impl WorkSpec {
/// Execute init-phase work. Returns a seed value.
pub fn do_init(&self) -> u64 {
if self.init_work > 0 { busy_work(self.init_work) } else { 0 }
}
/// Execute accumulate-phase work.
pub fn do_accumulate(&self, heap: &mut u64, child: &u64) {
if self.accumulate_work > 0 {
*heap = heap.wrapping_add(busy_work(self.accumulate_work));
}
*heap = heap.wrapping_add(*child);
}
/// Execute finalize-phase work.
pub fn do_finalize(&self, heap: &u64) -> u64 {
if self.finalize_work > 0 {
heap.wrapping_add(busy_work(self.finalize_work))
} else {
*heap
}
}
/// Execute graph-traversal work (child discovery).
pub fn do_graph(&self) {
spin_wait_us(self.graph_io_us);
if self.graph_work > 0 { black_box(busy_work(self.graph_work)); }
}
}
}
Funnel policy variants
#![allow(unused)]
fn main() {
impl FunnelSpecs {
pub fn new(nw: usize) -> Self {
use funnel::wake::once_per_batch::OncePerBatchSpec;
use funnel::wake::every_k::EveryKSpec;
FunnelSpecs {
// PW + Final × wake
pw_fin: funnel::Spec::default(nw),
pw_fin_batch: funnel::Spec::for_low_overhead(nw),
pw_fin_k4: funnel::Spec::for_high_throughput(nw),
pw_fin_k2: funnel::Spec::for_deep_narrow(nw),
// PW + Arrive × wake
pw_arrv: funnel::Spec::for_perworker_arrival(nw),
pw_arrv_batch: funnel::Spec::for_perworker_arrival(nw).with_wake::<wake::OncePerBatch>(OncePerBatchSpec),
pw_arrv_k4: funnel::Spec::for_perworker_arrival(nw).with_wake::<wake::EveryK<4>>(EveryKSpec),
pw_arrv_k2: funnel::Spec::for_perworker_arrival(nw).with_wake::<wake::EveryK<2>>(EveryKSpec),
// SH + Final × wake
sh_fin: funnel::Spec::for_shared_default(nw),
sh_fin_batch: funnel::Spec::for_shared_default(nw).with_wake::<wake::OncePerBatch>(OncePerBatchSpec),
sh_fin_k4: funnel::Spec::for_shared_default(nw).with_wake::<wake::EveryK<4>>(EveryKSpec),
sh_fin_k2: funnel::Spec::for_shared_default(nw).with_wake::<wake::EveryK<2>>(EveryKSpec),
// SH + Arrive × wake
sh_arrv: funnel::Spec::for_wide_light(nw),
sh_arrv_batch: funnel::Spec::for_streaming_wide(nw),
sh_arrv_k4: funnel::Spec::for_wide_light(nw).with_wake::<wake::EveryK<4>>(EveryKSpec),
sh_arrv_k2: funnel::Spec::for_wide_light(nw).with_wake::<wake::EveryK<2>>(EveryKSpec),
}
}
}
}
See Funnel policies for the meaning of each axis, the rationale, and guidance on selecting a preset.
Text tables
Overhead
workload fused hand.seq real.seq
-------------------------------------------------------------------------------
aggr_sm 42.9ms (best) 43.7ms (+2%) 42.9ms (best)
bal_sm 76.3ms (+2%) 75.0ms (best) 74.9ms (best)
deep_sm 29.9ms (best) 30.7ms (+3%) 30.4ms (+2%)
fin_sm 37.6ms (best) 37.5ms (best) 38.3ms (+2%)
graph-hv_sm 81.9ms (best) 83.5ms (+2%) 82.4ms (+1%)
graph-io_sm 66.7ms (best) 66.8ms (+1%) 66.3ms (best)
hash_sm 4.1ms (+1%) 4.0ms (best) 4.1ms (+1%)
io_sm 42.0ms (best) 41.9ms (best) 42.0ms (best)
lg-dense_sm 76.5ms (+2%) 75.3ms (+1%) 74.8ms (best)
noop_sm 0.0ms (+112%) 0.0ms (best) 0.0ms (+1018%)
parse-hv_sm 102.8ms (+2%) 102.1ms (+1%) 100.7ms (best)
parse-lt_sm 26.7ms (best) 27.4ms (+2%) 27.2ms (+2%)
wide_sm 30.5ms (best) 30.4ms (best) 30.5ms (best)
xform_sm 44.6ms (+3%) 43.4ms (best) 43.7ms (+1%)
Matrix
workload hand.rayon real.rayon rayonfunnel.pw.arrv.pushfunnel.pw.arrv.batch funnel.pw.arrv.k4 funnel.pw.arrv.k2funnel.sh.arrv.pushfunnel.sh.arrv.batch funnel.sh.arrv.k4 funnel.sh.arrv.k2funnel.pw.fin.pushfunnel.pw.fin.batch funnel.pw.fin.k4 funnel.pw.fin.k2funnel.sh.fin.pushfunnel.sh.fin.batch funnel.sh.fin.k4 funnel.sh.fin.k2
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
aggr_sm 13.7ms (+23%) 13.8ms (+24%) 13.1ms (+17%) 12.5ms (+12%) 12.5ms (+12%) 14.2ms (+27%) 12.8ms (+15%) 13.8ms (+23%) 15.8ms (+41%) 14.0ms (+25%) 15.3ms (+37%) 11.8ms (+6%) 13.4ms (+20%) 12.0ms (+8%) 13.3ms (+19%) 11.4ms (+2%) 11.2ms (best) 12.2ms (+9%) 12.6ms (+13%)
bal_sm 15.9ms (+2%) 18.2ms (+17%) 15.6ms (best) 18.3ms (+17%) 18.8ms (+21%) 18.6ms (+20%) 18.5ms (+19%) 17.9ms (+15%) 16.2ms (+4%) 17.0ms (+9%) 18.1ms (+16%) 18.5ms (+19%) 18.9ms (+21%) 18.1ms (+16%) 17.0ms (+9%) 16.9ms (+9%) 18.8ms (+21%) 18.8ms (+21%) 18.6ms (+20%)
deep_sm 7.2ms (+4%) 7.2ms (+4%) 7.2ms (+4%) 7.2ms (+4%) 7.2ms (+4%) 7.2ms (+4%) 7.2ms (+4%) 6.9ms (best) 6.9ms (best) 6.9ms (best) 6.9ms (best) 7.3ms (+6%) 7.3ms (+5%) 7.2ms (+5%) 7.2ms (+5%) 7.0ms (+1%) 7.0ms (+2%) 7.0ms (+2%) 7.0ms (+2%)
fin_sm 8.7ms (+6%) 8.7ms (+6%) 8.2ms (best) 8.5ms (+4%) 8.6ms (+4%) 8.6ms (+4%) 8.6ms (+4%) 8.5ms (+4%) 8.6ms (+4%) 8.5ms (+4%) 8.6ms (+4%) 8.6ms (+4%) 8.6ms (+4%) 8.6ms (+4%) 8.6ms (+4%) 8.6ms (+4%) 8.5ms (+4%) 8.6ms (+4%) 8.5ms (+4%)
graph-hv_sm 17.7ms (best) 17.8ms (+1%) 17.8ms (+1%) 18.0ms (+2%) 19.9ms (+13%) 20.4ms (+16%) 17.9ms (+1%) 20.4ms (+15%) 18.1ms (+2%) 19.8ms (+12%) 18.1ms (+2%) 18.1ms (+2%) 20.3ms (+15%) 21.2ms (+20%) 18.0ms (+2%) 18.1ms (+3%) 20.6ms (+16%) 18.5ms (+5%) 20.4ms (+15%)
graph-io_sm 11.4ms (+1%) 12.5ms (+10%) 11.5ms (+1%) 11.4ms (+1%) 12.9ms (+14%) 12.3ms (+8%) 12.1ms (+7%) 11.3ms (best) 12.6ms (+12%) 11.9ms (+5%) 11.4ms (best) 11.6ms (+2%) 11.9ms (+5%) 12.9ms (+14%) 12.1ms (+7%) 11.5ms (+2%) 13.0ms (+15%) 12.2ms (+8%) 11.6ms (+2%)
hash_sm 1.0ms (+3%) 1.0ms (+3%) 1.0ms (+6%) 1.0ms (+1%) 1.0ms (+2%) 0.9ms (best) 0.9ms (+1%) 0.9ms (best) 1.0ms (+4%) 1.0ms (+3%) 1.0ms (+3%) 1.0ms (+6%) 1.0ms (+8%) 1.0ms (+6%) 1.0ms (+6%) 1.0ms (+7%) 1.0ms (+8%) 1.0ms (+5%) 1.0ms (+6%)
io_sm 5.8ms (best) 5.8ms (best) 5.8ms (best) 5.8ms (+1%) 6.2ms (+7%) 6.0ms (+4%) 6.0ms (+4%) 5.8ms (best) 6.2ms (+7%) 6.0ms (+4%) 6.0ms (+4%) 5.8ms (best) 6.2ms (+7%) 6.0ms (+4%) 6.0ms (+4%) 5.8ms (+1%) 6.2ms (+7%) 6.0ms (+4%) 6.0ms (+4%)
lg-dense_sm 16.1ms (+6%) 16.1ms (+6%) 16.1ms (+6%) 16.2ms (+7%) 16.4ms (+8%) 16.3ms (+7%) 16.4ms (+8%) 16.1ms (+6%) 16.3ms (+8%) 16.2ms (+6%) 16.2ms (+7%) 19.1ms (+26%) 15.2ms (best) 19.1ms (+26%) 16.4ms (+8%) 18.8ms (+24%) 15.4ms (+2%) 16.3ms (+7%) 16.3ms (+7%)
noop_sm 0.0ms (best) 0.0ms (best) 0.0ms (best) 0.1ms (+102%) 0.0ms (+75%) 0.0ms (+69%) 0.0ms (+91%) 0.1ms (+168%) 0.1ms (+152%) 0.1ms (+157%) 0.1ms (+158%) 0.1ms (+109%) 0.0ms (+72%) 0.0ms (+77%) 0.0ms (+89%) 0.1ms (+167%) 0.1ms (+163%) 0.1ms (+162%) 0.1ms (+228%)
parse-hv_sm 21.7ms (+10%) 20.5ms (+4%) 21.8ms (+10%) 21.9ms (+11%) 19.7ms (best) 22.4ms (+13%) 22.1ms (+12%) 21.8ms (+10%) 21.8ms (+10%) 20.7ms (+5%) 20.6ms (+4%) 20.6ms (+4%) 21.2ms (+8%) 21.0ms (+6%) 22.2ms (+13%) 22.0ms (+12%) 22.8ms (+16%) 22.4ms (+13%) 22.2ms (+12%)
parse-lt_sm 5.9ms (+20%) 5.9ms (+20%) 5.9ms (+20%) 5.9ms (+20%) 6.1ms (+24%) 6.1ms (+23%) 6.0ms (+22%) 5.8ms (+19%) 6.0ms (+22%) 4.9ms (best) 5.5ms (+12%) 5.7ms (+16%) 5.8ms (+18%) 5.8ms (+18%) 5.7ms (+17%) 6.0ms (+22%) 6.1ms (+25%) 6.1ms (+24%) 6.1ms (+23%)
wide_sm 6.6ms (best) 7.8ms (+18%) 7.8ms (+18%) 6.8ms (+2%) 8.3ms (+25%) 7.0ms (+7%) 6.8ms (+2%) 6.8ms (+4%) 7.1ms (+8%) 6.9ms (+4%) 6.8ms (+4%) 7.3ms (+11%) 8.1ms (+23%) 7.3ms (+10%) 7.3ms (+11%) 7.4ms (+13%) 7.6ms (+15%) 7.4ms (+12%) 7.4ms (+12%)
xform_sm 9.1ms (best) 9.3ms (+2%) 9.4ms (+3%) 9.3ms (+2%) 9.6ms (+5%) 9.9ms (+8%) 9.9ms (+8%) 9.7ms (+7%) 9.8ms (+7%) 9.8ms (+7%) 9.7ms (+7%) 9.9ms (+9%) 9.9ms (+9%) 9.9ms (+9%) 9.9ms (+9%) 9.9ms (+8%) 9.9ms (+9%) 9.9ms (+9%) 9.9ms (+8%)
Module simulation
workload vanilla.rayon rayonfunnel.pw.arrv.pushfunnel.pw.arrv.batch funnel.pw.arrv.k4 funnel.pw.arrv.k2funnel.sh.arrv.pushfunnel.sh.arrv.batch funnel.sh.arrv.k4 funnel.sh.arrv.k2funnel.pw.fin.pushfunnel.pw.fin.batch funnel.pw.fin.k4 funnel.pw.fin.k2funnel.sh.fin.pushfunnel.sh.fin.batch funnel.sh.fin.k4 funnel.sh.fin.k2
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
large-dense_fast 1.2ms (+15%) 1.2ms (+12%) 1.2ms (+13%) 1.2ms (+15%) 1.1ms (+6%) 1.1ms (+7%) 1.2ms (+11%) 1.2ms (+13%) 1.2ms (+12%) 1.2ms (+11%) 1.2ms (+16%) 1.2ms (+17%) 1.2ms (+17%) 1.1ms (+3%) 1.0ms (best) 1.2ms (+18%) 1.2ms (+14%) 1.2ms (+15%)
large-dense_slow 22.0ms (+10%) 21.9ms (+10%) 22.2ms (+11%) 21.7ms (+8%) 22.7ms (+14%) 21.9ms (+9%) 20.0ms (best) 22.9ms (+15%) 21.3ms (+6%) 22.4ms (+12%) 22.2ms (+11%) 21.4ms (+7%) 22.8ms (+14%) 21.7ms (+9%) 21.2ms (+6%) 22.0ms (+10%) 22.0ms (+10%) 20.5ms (+3%)
large-sparse_fast 1.2ms (+21%) 1.0ms (best) 1.2ms (+20%) 1.2ms (+21%) 1.2ms (+21%) 1.2ms (+20%) 1.2ms (+20%) 1.0ms (+6%) 1.2ms (+20%) 1.2ms (+19%) 1.2ms (+21%) 1.2ms (+23%) 1.1ms (+10%) 1.2ms (+21%) 1.2ms (+20%) 1.2ms (+21%) 1.2ms (+22%) 1.2ms (+20%)
large-sparse_slow 18.4ms (best) 21.0ms (+14%) 22.8ms (+24%) 21.7ms (+18%) 23.2ms (+26%) 21.7ms (+18%) 20.7ms (+12%) 23.0ms (+25%) 21.8ms (+18%) 22.0ms (+20%) 22.8ms (+24%) 21.0ms (+14%) 23.2ms (+26%) 21.4ms (+16%) 22.6ms (+23%) 22.1ms (+20%) 22.2ms (+21%) 22.4ms (+22%)
small-dense_fast 0.4ms (+38%) 0.4ms (+35%) 0.4ms (+31%) 0.3ms (+17%) 0.4ms (+38%) 0.4ms (+18%) 0.3ms (+14%) 0.4ms (+44%) 0.3ms (best) 0.4ms (+28%) 0.4ms (+34%) 0.4ms (+32%) 0.4ms (+41%) 0.4ms (+22%) 0.4ms (+18%) 0.4ms (+39%) 0.4ms (+35%) 0.4ms (+33%)
small-dense_slow 6.0ms (+4%) 6.0ms (+3%) 5.8ms (+1%) 8.0ms (+38%) 6.2ms (+7%) 5.9ms (+3%) 5.8ms (best) 7.1ms (+24%) 6.1ms (+6%) 5.9ms (+2%) 5.9ms (+2%) 7.6ms (+32%) 6.2ms (+7%) 6.0ms (+4%) 5.9ms (+2%) 7.3ms (+27%) 6.2ms (+7%) 6.0ms (+4%)
small-sparse_fast 0.4ms (+50%) 0.4ms (+34%) 0.4ms (+49%) 0.4ms (+28%) 0.4ms (+50%) 0.4ms (+49%) 0.3ms (best) 0.4ms (+49%) 0.4ms (+34%) 0.4ms (+33%) 0.4ms (+43%) 0.3ms (+27%) 0.4ms (+30%) 0.4ms (+53%) 0.3ms (+27%) 0.3ms (+27%) 0.4ms (+50%) 0.4ms (+32%)
small-sparse_slow 6.6ms (+11%) 6.0ms (best) 6.5ms (+9%) 6.5ms (+10%) 6.5ms (+10%) 6.5ms (+9%) 6.1ms (+2%) 6.5ms (+9%) 6.5ms (+9%) 6.1ms (+2%) 6.5ms (+9%) 6.6ms (+10%) 6.6ms (+10%) 6.5ms (+9%) 6.1ms (+2%) 6.5ms (+9%) 6.5ms (+9%) 6.1ms (+2%)
Quick
workload real.rayon funnel.pw.arrv.k4 funnel.sh.arrv.k4 funnel.pw.fin.k4 funnel.sh.fin.k4
-------------------------------------------------------------------------------------------------------------------
aggr_sm 13.4ms (+4%) 13.9ms (+8%) 15.6ms (+21%) 13.0ms (+2%) 12.8ms (best)
bal_sm 18.8ms (+2%) 18.7ms (+1%) 18.9ms (+2%) 18.5ms (best) 18.4ms (best)
graph-hv_sm 18.6ms (best) 18.8ms (+1%) 18.7ms (best) 18.9ms (+2%) 18.7ms (+1%)
hash_sm 1.1ms (+3%) 1.0ms (best) 1.0ms (best) 1.1ms (+1%) 1.1ms (+2%)
noop_sm 0.0ms (best) 0.0ms (+87%) 0.1ms (+184%) 0.0ms (+83%) 0.1ms (+183%)
parse-hv_sm 22.5ms (+2%) 22.1ms (best) 22.0ms (best) 22.1ms (best) 22.0ms (best)
parse-lt_sm 6.3ms (+2%) 6.2ms (+1%) 6.1ms (best) 6.2ms (+2%) 6.2ms (best)
wide_sm 7.9ms (+7%) 7.4ms (best) 7.4ms (best) 7.5ms (+2%) 7.5ms (+2%)
xform_sm 10.5ms (+4%) 10.2ms (+1%) 10.1ms (best) 10.3ms (+1%) 10.2ms (best)
Benchmark source
Overhead harness
#![allow(unused)]
fn main() {
//! Overhead — framework cost.
//! Fused executor vs handrolled recursive baselines. No parallelism.
//! Reproduce: make -C hylic-benchmark _bench-overhead
#[path = "support/mod.rs"]
mod support;
use criterion::{criterion_group, criterion_main, Criterion};
use std::hint::black_box;
use support::scenario::{self, Scale, PreparedScenario};
use support::{baselines, bench_cell};
fn bench_overhead(c: &mut Criterion) {
let mut group = c.benchmark_group("overhead");
for def in scenario::all_scenarios(Scale::from_env()) {
let s = PreparedScenario::from_def(&def, "sm");
let p = s.as_problem();
let all = vec![
baselines::fused(&p),
baselines::hand_seq(&s),
baselines::real_seq(&s),
];
for r in &all {
bench_cell(&mut group, r.name, &p.name,
|b, _| b.iter(|| black_box((r.run)())),
);
}
}
group.finish();
}
criterion_group!(benches, bench_overhead);
criterion_main!(benches);
}
Matrix harness
#![allow(unused)]
fn main() {
//! Matrix — full executor comparison.
//! 16 funnel policy variants × 14 workload scenarios + all baselines.
//! Reproduce: make bench-compare (or make -C hylic-benchmark _bench-matrix)
#[path = "support/mod.rs"]
mod support;
use criterion::{criterion_group, criterion_main, Criterion};
use std::hint::black_box;
use hylic::exec::funnel;
use support::scenario::{self, Scale, PreparedScenario};
use support::executor_set::{ExecutorSet, FunnelSpecs};
use support::{runners, baselines, bench_cell};
fn bench_matrix(c: &mut Criterion) {
let nw = support::config::bench_workers();
funnel::Pool::with(nw, |fpool| {
let es = ExecutorSet { fpool, nw, funnel: FunnelSpecs::new(nw) };
let mut group = c.benchmark_group("matrix");
for def in scenario::all_scenarios(Scale::from_env()) {
let s = PreparedScenario::from_def(&def, "sm");
let p = s.as_problem();
let mut all = runners::all_hylic_runners(&p, &es);
all.extend(baselines::hand_baselines(&s));
for r in &all {
bench_cell(&mut group, r.name, &p.name,
|b, _| b.iter(|| black_box((r.run)())),
);
}
}
group.finish();
});
}
criterion_group!(benches, bench_matrix);
criterion_main!(benches);
}
Module simulation harness
#![allow(unused)]
fn main() {
//! Module simulation — realistic workload.
//! Dependency graph resolution with simulated file parsing and I/O.
//! Reproduce: make -C hylic-benchmark _bench-modsim
#[path = "support/mod.rs"]
mod support;
use criterion::{criterion_group, criterion_main, Criterion};
use std::hint::black_box;
use hylic::exec::funnel;
use support::executor_set::{ExecutorSet, FunnelSpecs};
use support::{module_sim, runners, bench_cell};
fn bench_modsim(c: &mut Criterion) {
let nw = support::config::bench_workers();
funnel::Pool::with(nw, |fpool| {
let es = ExecutorSet { fpool, nw, funnel: FunnelSpecs::new(nw) };
let mut group = c.benchmark_group("modsim");
for spec in module_sim::all_module_scenarios(false) {
let sim = module_sim::prepare(&spec);
let p = module_sim::as_problem(&sim);
let mut all = runners::all_hylic_runners(&p, &es);
all.extend(module_sim::vanilla_baselines(&sim));
for r in &all {
bench_cell(&mut group, r.name, &p.name,
|b, _| b.iter(|| black_box((r.run)())),
);
}
}
group.finish();
});
}
criterion_group!(benches, bench_modsim);
criterion_main!(benches);
}
Runner matrix construction
#![allow(unused)]
fn main() {
//! Benchmark runners — uniform constructors generic over node type.
//!
//! Every runner is `Runner<'a> = { name, Box<dyn Fn() -> u64> }`.
//! Hylic runners and handrolled baselines share the same type.
//! All hylic runners are generic over N — the same code serves
//! NodeId scenarios and String module-sim scenarios.
use hylic::domain::shared as dom;
use hylic::exec::funnel;
use hylic::exec::funnel::policy::FunnelPolicy;
use super::problem::BenchProblem;
use super::executor_set::ExecutorSet;
// ── Runner type ─────────────────────────────────────
pub struct Runner<'a> {
pub name: &'static str,
pub run: Box<dyn Fn() -> u64 + 'a>,
}
// ── Direct executors (generic over N) ───────────────
pub fn rayon<'a, N: Clone + Send + Sync + 'static>(p: &'a BenchProblem<N>) -> Runner<'a> {
let exec = dom::exec(hylic_benchmark::executor::rayon::Spec);
Runner { name: "rayon", run: Box::new(move || exec.run(&p.fold, &p.treeish, &p.root)) }
}
pub fn funnel_variant<'a, N: Clone + Send + 'static, P: FunnelPolicy>(
name: &'static str, p: &'a BenchProblem<N>, es: &'a ExecutorSet, spec: funnel::Spec<P>,
) -> Runner<'a> {
let exec = dom::exec(spec).attach(es.fpool);
Runner { name, run: Box::new(move || exec.run(&p.fold, &p.treeish, &p.root)) }
}
// ── Grouped constructors ────────────────────────────
/// All 16 funnel policy variants: 4 queue×accumulate × 4 wake.
pub fn funnel_runners<'a, N: Clone + Send + 'static>(
p: &'a BenchProblem<N>, es: &'a ExecutorSet,
) -> Vec<Runner<'a>> {
let s = &es.funnel;
vec![
// PerWorker + OnArrival × wake
funnel_variant("funnel.pw.arrv.push", p, es, s.pw_arrv),
funnel_variant("funnel.pw.arrv.batch", p, es, s.pw_arrv_batch),
funnel_variant("funnel.pw.arrv.k4", p, es, s.pw_arrv_k4),
funnel_variant("funnel.pw.arrv.k2", p, es, s.pw_arrv_k2),
// Shared + OnArrival × wake
funnel_variant("funnel.sh.arrv.push", p, es, s.sh_arrv),
funnel_variant("funnel.sh.arrv.batch", p, es, s.sh_arrv_batch),
funnel_variant("funnel.sh.arrv.k4", p, es, s.sh_arrv_k4),
funnel_variant("funnel.sh.arrv.k2", p, es, s.sh_arrv_k2),
// PerWorker + OnFinalize × wake
funnel_variant("funnel.pw.fin.push", p, es, s.pw_fin),
funnel_variant("funnel.pw.fin.batch", p, es, s.pw_fin_batch),
funnel_variant("funnel.pw.fin.k4", p, es, s.pw_fin_k4),
funnel_variant("funnel.pw.fin.k2", p, es, s.pw_fin_k2),
// Shared + OnFinalize × wake
funnel_variant("funnel.sh.fin.push", p, es, s.sh_fin),
funnel_variant("funnel.sh.fin.batch", p, es, s.sh_fin_batch),
funnel_variant("funnel.sh.fin.k4", p, es, s.sh_fin_k4),
funnel_variant("funnel.sh.fin.k2", p, es, s.sh_fin_k2),
]
}
/// All hylic runners: baselines + full funnel matrix.
pub fn all_hylic_runners<'a, N: Clone + Send + Sync + 'static>(
p: &'a BenchProblem<N>, es: &'a ExecutorSet,
) -> Vec<Runner<'a>> {
let mut v = vec![rayon(p)];
v.extend(funnel_runners(p, es));
v
}
}
Handrolled baselines
#![allow(unused)]
fn main() {
//! Handrolled baselines — problem-specific, not generic over N.
//!
//! These bypass the hylic fold/treeish abstraction entirely,
//! operating on raw adjacency lists or domain-specific data.
//! They exist to measure framework overhead.
use super::runners::Runner;
use super::problem::BenchProblem;
use super::tree::NodeId;
use super::work::WorkSpec;
use super::scenario::PreparedScenario;
// ── Fused executor (hylic sequential baseline) ──────
pub fn fused<'a, N: Clone + 'static>(p: &'a BenchProblem<N>) -> Runner<'a> {
use hylic::domain::shared as dom;
Runner { name: "fused", run: Box::new(|| dom::FUSED.run(&p.fold, &p.treeish, &p.root)) }
}
// ── Handrolled baselines (no hylic, raw recursion) ──
pub fn hand_seq<'a>(s: &'a PreparedScenario) -> Runner<'a> {
Runner { name: "hand.seq", run: Box::new(|| handrolled_seq(s)) }
}
pub fn hand_rayon<'a>(s: &'a PreparedScenario) -> Runner<'a> {
Runner { name: "hand.rayon", run: Box::new(|| handrolled_rayon(s)) }
}
pub fn real_seq<'a>(s: &'a PreparedScenario) -> Runner<'a> {
Runner { name: "real.seq", run: Box::new(|| realworld_seq(s)) }
}
pub fn real_rayon<'a>(s: &'a PreparedScenario) -> Runner<'a> {
Runner { name: "real.rayon", run: Box::new(|| realworld_rayon(s)) }
}
/// Parallel handrolled baselines for the matrix benchmark.
/// Sequential baselines (hand.seq, real.seq) are in the overhead suite only.
pub fn hand_baselines<'a>(s: &'a PreparedScenario) -> Vec<Runner<'a>> {
vec![hand_rayon(s), real_rayon(s)]
}
// ── Implementations ─────────────────────────────────
fn handrolled_seq(s: &PreparedScenario) -> u64 {
fn recurse(children: &[Vec<NodeId>], work: &WorkSpec, node: NodeId) -> u64 {
work.do_graph();
let mut heap = work.do_init();
for &child in &children[node] {
work.do_accumulate(&mut heap, &recurse(children, work, child));
}
work.do_finalize(&heap)
}
recurse(&s.children, &s.work, s.root)
}
fn handrolled_rayon(s: &PreparedScenario) -> u64 {
use rayon::prelude::*;
fn recurse(children: &[Vec<NodeId>], work: &WorkSpec, node: NodeId) -> u64 {
work.do_graph();
let mut heap = work.do_init();
let ch = &children[node];
if ch.len() <= 1 {
for &child in ch {
work.do_accumulate(&mut heap, &recurse(children, work, child));
}
} else {
let results: Vec<u64> = ch.par_iter()
.map(|&c| recurse(children, work, c))
.collect();
for r in &results { work.do_accumulate(&mut heap, r); }
}
work.do_finalize(&heap)
}
recurse(&s.children, &s.work, s.root)
}
fn realworld_seq(s: &PreparedScenario) -> u64 {
use std::collections::HashMap;
fn resolve(children: &[Vec<NodeId>], work: &WorkSpec, node: NodeId, cache: &mut HashMap<NodeId, u64>) -> u64 {
if let Some(&v) = cache.get(&node) { return v; }
work.do_graph();
let mut heap = work.do_init();
for &child in &children[node] {
work.do_accumulate(&mut heap, &resolve(children, work, child, cache));
}
let result = work.do_finalize(&heap);
cache.insert(node, result);
result
}
let mut cache = HashMap::new();
resolve(&s.children, &s.work, s.root, &mut cache)
}
fn realworld_rayon(s: &PreparedScenario) -> u64 {
use rayon::prelude::*;
fn resolve(children: &[Vec<NodeId>], work: &WorkSpec, node: NodeId) -> u64 {
work.do_graph();
let mut heap = work.do_init();
let ch = &children[node];
if ch.len() <= 1 {
for &child in ch {
work.do_accumulate(&mut heap, &resolve(children, work, child));
}
} else {
let results: Vec<u64> = ch.par_iter()
.map(|&c| resolve(children, work, c))
.collect();
for r in &results { work.do_accumulate(&mut heap, r); }
}
work.do_finalize(&heap)
}
resolve(&s.children, &s.work, s.root)
}
}
Funnel policy specs
#![allow(unused)]
fn main() {
//! ExecutorSet: shared resources for all benchmark runners.
use hylic::exec::funnel;
use hylic::exec::funnel::policy;
use hylic::exec::funnel::wake;
/// All 16 funnel policy variants: 4 queue×accumulate × 4 wake.
pub struct FunnelSpecs {
// PerWorker + OnFinalize × 4 wake
pub pw_fin: funnel::Spec<policy::Default>,
pub pw_fin_batch: funnel::Spec<policy::LowOverhead>,
pub pw_fin_k4: funnel::Spec<policy::HighThroughput>,
pub pw_fin_k2: funnel::Spec<policy::DeepNarrow>,
// PerWorker + OnArrival × 4 wake
pub pw_arrv: funnel::Spec<policy::PerWorkerArrival>,
pub pw_arrv_batch: funnel::Spec<policy::Policy<funnel::queue::PerWorker, funnel::accumulate::OnArrival, wake::OncePerBatch>>,
pub pw_arrv_k4: funnel::Spec<policy::Policy<funnel::queue::PerWorker, funnel::accumulate::OnArrival, wake::EveryK<4>>>,
pub pw_arrv_k2: funnel::Spec<policy::Policy<funnel::queue::PerWorker, funnel::accumulate::OnArrival, wake::EveryK<2>>>,
// Shared + OnFinalize × 4 wake
pub sh_fin: funnel::Spec<policy::SharedDefault>,
pub sh_fin_batch: funnel::Spec<policy::Policy<funnel::queue::Shared, funnel::accumulate::OnFinalize, wake::OncePerBatch>>,
pub sh_fin_k4: funnel::Spec<policy::Policy<funnel::queue::Shared, funnel::accumulate::OnFinalize, wake::EveryK<4>>>,
pub sh_fin_k2: funnel::Spec<policy::Policy<funnel::queue::Shared, funnel::accumulate::OnFinalize, wake::EveryK<2>>>,
// Shared + OnArrival × 4 wake
pub sh_arrv: funnel::Spec<policy::WideLight>,
pub sh_arrv_batch: funnel::Spec<policy::StreamingWide>,
pub sh_arrv_k4: funnel::Spec<policy::Policy<funnel::queue::Shared, funnel::accumulate::OnArrival, wake::EveryK<4>>>,
pub sh_arrv_k2: funnel::Spec<policy::Policy<funnel::queue::Shared, funnel::accumulate::OnArrival, wake::EveryK<2>>>,
}
// ANCHOR: funnel_specs
impl FunnelSpecs {
pub fn new(nw: usize) -> Self {
use funnel::wake::once_per_batch::OncePerBatchSpec;
use funnel::wake::every_k::EveryKSpec;
FunnelSpecs {
// PW + Final × wake
pw_fin: funnel::Spec::default(nw),
pw_fin_batch: funnel::Spec::for_low_overhead(nw),
pw_fin_k4: funnel::Spec::for_high_throughput(nw),
pw_fin_k2: funnel::Spec::for_deep_narrow(nw),
// PW + Arrive × wake
pw_arrv: funnel::Spec::for_perworker_arrival(nw),
pw_arrv_batch: funnel::Spec::for_perworker_arrival(nw).with_wake::<wake::OncePerBatch>(OncePerBatchSpec),
pw_arrv_k4: funnel::Spec::for_perworker_arrival(nw).with_wake::<wake::EveryK<4>>(EveryKSpec),
pw_arrv_k2: funnel::Spec::for_perworker_arrival(nw).with_wake::<wake::EveryK<2>>(EveryKSpec),
// SH + Final × wake
sh_fin: funnel::Spec::for_shared_default(nw),
sh_fin_batch: funnel::Spec::for_shared_default(nw).with_wake::<wake::OncePerBatch>(OncePerBatchSpec),
sh_fin_k4: funnel::Spec::for_shared_default(nw).with_wake::<wake::EveryK<4>>(EveryKSpec),
sh_fin_k2: funnel::Spec::for_shared_default(nw).with_wake::<wake::EveryK<2>>(EveryKSpec),
// SH + Arrive × wake
sh_arrv: funnel::Spec::for_wide_light(nw),
sh_arrv_batch: funnel::Spec::for_streaming_wide(nw),
sh_arrv_k4: funnel::Spec::for_wide_light(nw).with_wake::<wake::EveryK<4>>(EveryKSpec),
sh_arrv_k2: funnel::Spec::for_wide_light(nw).with_wake::<wake::EveryK<2>>(EveryKSpec),
}
}
}
// ANCHOR_END: funnel_specs
/// Shared resources for a benchmark session. Constructed once, passed to all runners.
pub struct ExecutorSet<'a> {
pub fpool: &'a funnel::Pool<'a>,
pub nw: usize,
pub funnel: FunnelSpecs,
}
}
Correctness
Performance numbers are uninformative without correctness. The
Funnel executor has a unit and integration suite under
hylic/src/exec/variant/funnel/tests/ covering the API, parity
with the Fused baseline, and deterministic results across all
policy variants. An interleaving stress harness in
tests/interleaving.rs and tests/stress.rs exercises the
scheduler under aggressive steal patterns. Every benchmark
harness asserts that the computed R matches a reference Fused
run (PreparedScenario::expected) before timing begins; a
policy variant producing a faster-but-incorrect answer would
never reach the tables above.