Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Parallel execution

hylic provides two built-in executors. FUSED runs the fold sequentially through callback-based recursion. The Funnel executor parallelizes the same fold across a scoped thread pool. Both are invoked through the same .run() method.

Sequential: FUSED

Callback-based recursion on a single thread, with no overhead beyond the fold closures themselves:

#![allow(unused)]
fn main() {
use hylic::prelude::*;
FUSED.run(&fold, &graph, &root);
}

Parallel: Funnel

The Funnel executor preserves the fused property — children are discovered through graph.visit and processed concurrently. No intermediate tree is built.

One-shot

#![allow(unused)]
fn main() {
use hylic::prelude::*;
exec(funnel::Spec::default(8)).run(&fold, &graph, &root);
}

Spec::default(n) uses the Robust policy preset. .run() creates a scoped thread pool internally, runs the fold, and joins before returning.

Session scope

For repeated folds, amortize pool creation:

#![allow(unused)]
fn main() {
exec(funnel::Spec::default(8)).session(|s| {
    s.run(&fold1, &graph1, &root1);
    s.run(&fold2, &graph2, &root2);
});
}

The pool lives for the closure. Each .run() inside is cheap.

Explicit attach

Provide the pool yourself:

#![allow(unused)]
fn main() {
funnel::Pool::with(8, |pool| {
    let pw = exec(funnel::Spec::default(8)).attach(pool);
    let sh = exec(funnel::Spec::for_wide_light(8)).attach(pool);
    pw.run(&fold, &graph, &root);
    sh.run(&fold, &graph, &root);
});
}

Different policies can share a pool — each .attach() consumes a (Copy) Spec and binds it to the pool, producing a session-level executor.

Policy variants

PresetBest for
Spec::default(n)General purpose
Spec::for_wide_light(n)Wide trees (bf > 10)
Spec::for_deep_narrow(n)Deep chains (bf = 2)
Spec::for_low_overhead(n)Overhead-sensitive
Spec::for_high_throughput(n)Heavy balanced

See Funnel policies for the full decision guide and The Exec pattern for the type-level design behind .run(), .session(), and .attach().

External parallel options

One additional strategy lives in a sibling crate:

  • Rayon (hylic-benchmark): par_iter-based fork-join

Working example

This example uses a flat adjacency list — nodes are integer indices, children are looked up by index. The same fold runs sequentially (Fused) and in parallel (Funnel) with identical results.

#![allow(unused)]
fn main() {
//! Parallel execution: Fused vs Funnel over flat data.
//! Demonstrates: adjacency-list graph, identical results across
//! policies, session scope, explicit pool attach.

#[cfg(test)]
mod tests {
    use hylic::prelude::*;
    use hylic::exec::funnel;
    use insta::assert_snapshot;

    /// Build a tree as a flat adjacency list + value array.
    /// Node 0 is the root with 6 children; each child has 3 leaves.
    fn build_tree() -> (Vec<Vec<usize>>, Vec<u64>) {
        let mut adj: Vec<Vec<usize>> = Vec::new();
        let mut vals: Vec<u64> = Vec::new();

        // root (node 0)
        adj.push((1..=6).collect());
        vals.push(1);

        // 6 branches (nodes 1-6), each with 3 leaves
        let mut next_leaf = 7;
        for i in 0..6 {
            let children: Vec<usize> = (next_leaf..next_leaf + 3).collect();
            adj.push(children);
            vals.push(i as u64 * 10);
            next_leaf += 3;
        }

        // 18 leaves (nodes 7-24)
        for i in 0..6 {
            for j in 0..3u64 {
                adj.push(vec![]);
                vals.push(i as u64 * 10 + j);
            }
        }

        (adj, vals)
    }

    #[test]
    fn parallel_strategies() {
        let (adj, vals) = build_tree();

        // The treeish looks up children by index; no nested structs.
        let adj_for_graph = adj.clone();
        let graph: Treeish<usize> = treeish_visit(move |n: &usize, cb: &mut dyn FnMut(&usize)| {
            for &c in &adj_for_graph[*n] { cb(&c); }
        });

        let vals_for_fold = vals.clone();
        let sum: Fold<usize, u64, u64> = fold(
            move |n: &usize| vals_for_fold[*n],
            |heap: &mut u64, child: &u64| *heap += child,
            |heap: &u64| *heap,
        );

        // Sequential baseline
        let expected = FUSED.run(&sum, &graph, &0usize);

        // One-shot: .run() creates + destroys pool internally
        let r_default = exec(funnel::Spec::default(4)).run(&sum, &graph, &0usize);
        assert_eq!(r_default, expected);

        // Different policy: wide-light
        let r_wide = exec(funnel::Spec::for_wide_light(4)).run(&sum, &graph, &0usize);
        assert_eq!(r_wide, expected);

        // Session scope: pool shared across folds
        exec(funnel::Spec::default(4)).session(|s| {
            assert_eq!(s.run(&sum, &graph, &0usize), expected);
            assert_eq!(s.run(&sum, &graph, &0usize), expected);
        });

        // Explicit attach: manual pool, multiple policies
        funnel::Pool::with(4, |pool| {
            let pw = exec(funnel::Spec::default(4)).attach(pool);
            let sh = exec(funnel::Spec::for_wide_light(4)).attach(pool);
            assert_eq!(pw.run(&sum, &graph, &0usize), expected);
            assert_eq!(sh.run(&sum, &graph, &0usize), expected);
        });

        assert_snapshot!("parallel", format!(
            "sum = {expected}, verified: fused, funnel(one-shot), funnel(wide), session, attach"
        ));
    }
}
}

Output:

sum = 619, verified: fused, funnel(one-shot), funnel(wide), session, attach