Skip to content

Investigate + benchmark: fork-join parallel-map/fold over std.worker (Bend-inspired), before any stdlib commitment #1297

Description

@nicolas-maman

Motivation

Higher Order Co's Bend/HVM (https://higherorderco.com) gets automatic parallelism by running everything on an interaction-combinator graph reducer. We deliberately do NOT adopt that engine, it is a VM with GC-adjacent semantics, the exact runtime our no-VM / no-GC / compile-to-native identity rejects (same conclusion recorded for Fil-C's FUGC and Objective-S's metaobject protocol).

What DOES port is the ergonomic idea, not the runtime: parallelism expressed as structure, not thread code ("write divide-and-conquer, get multi-core for free"). We can likely deliver the realistic 80% of that as a thin library over machinery we already shipped, but it must earn its place on measured numbers, not vibes. This issue is to spike + benchmark + test, then make a go/no-go call. No stdlib commitment until the data justifies it.

What already exists (so this is a library layer, not a new runtime)

  • std.worker bounded thread pool: run submits to it; pool_size / aether_worker_pool_configure / pool_shutdown. Off-scheduler, so it does not starve actor cores.
  • Isolated[T] (Language: Isolated[T] wrapper for actor message types (Nim-inspired) #479): move-only, compile-time-checked, zero runtime cost. This is the linearity that makes fork-join race-free by construction (share nothing, move ownership across the split), the same discipline that lets interaction nets run lock-free.
  • docs/structured-concurrency.md for the conceptual home.

Missing: any parallel_map / parallel fold combinator.

Spike (branch only, not merged until numbers land)

  • Prototype worker.parallel_map(list, f) and a parallel fold/reduce that dispatches independent branches onto the existing bounded pool.
  • Payloads carried as Isolated[T] so subtasks cannot share mutable state; misuse is a compile error via the existing move checker.
  • Auto-fallback to sequential below a measured threshold N (see benchmark 2).

Benchmarks to run (the actual deliverable)

  1. Speedup vs core count. CPU-bound divide-and-conquer (e.g. parallel mergesort, mandelbrot tiles, or an n-body step). Wall-time at pool_size 1/2/4/8/ncores. Target: near-linear up to core count.
  2. Overhead floor / crossover. Same op sequential vs parallel at small N. Find the N where parallel first wins; that threshold becomes the auto-sequential fallback point. If the crossover is huge, the layer is not worth it.
  3. vs hand-rolled actors. Compare against the same fan-out done with spawn + ?/reply. The combinator must not be slower than rolling it by hand, or it is just sugar with a tax.
  4. Safety under sanitizers. ASan/UBSan/LSan clean, pool joins cleanly, no leaks across thousands of iterations; confirm the Isolated move-checker rejects a shared-capture attempt at compile time.

Decision criteria (go / no-go)

Ship ONLY if: near-linear speedup on CPU-bound work, a small-enough crossover N to be broadly useful, zero leaks/races, and the call site reads declaratively (structure, not thread handles). Otherwise document the finding and close.

Explicit non-goals (do not port, they need the runtime we reject): the interaction-combinator engine, optimal reduction, the GPU/CUDA backend. If a real workload ever needs true HVM-grade parallelism, the Aether-shaped move is to HOST HVM as a guest (contrib.host.*), not reimplement it, and only after a license check.

Artifacts

  • Benchmark harness under benchmarks/ (or tests/).
  • A short findings note in the docs/cross-references/ style (bend.md), recording the numbers and the go/no-go.
  • If go: a follow-up implementation issue with the measured threshold baked into the API.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions