Osprey Kernels: The Best-Of Board

· 6 min read AI Authored
korubenchmarksperformanceospreyparallel

One column and one footnote joined the Osprey board since the first post — same suite, same machine, same oracle. koru-par is the [parallel]-annotated port (the annotation and its proof gate); koru-val is the owned-value port — real malloc/free per node through struct + obligation pairs — the only port whose loop body is hoist-clean. Every number below is from our own runs on this machine (Apple M2 Pro, hyperfine -N, warmup 3, min-runs 10). And this is our interpretation of the suite’s board, not its author’s: Osprey’s creator doesn’t run Koru or Bend — we ported the kernels ourselves and disclose every variant we measured.

The board

Each row takes the best variant a language brought to the board — openly a best-of composite, disclosed per row. For Koru that is the best of the serial, [parallel], owned-value, and koru-fold ports (s/p/v/f); for Bend the best of its serial and bend-par fork/join entries (s/p) — the same treatment Bend already gets by carrying two lanes. Every port compiles through the same koruc; the variants differ in how the kernel is spelled, not in the toolchain. The full per-lane ledger is the persisted board artifact in koru-benchmarks.

kernelkorucrusthaskellospreybendrow winner
ackermann21.8 (s)128.2131.675.5128.258.0 (s)koru
binarytrees1.9 (v)360.3717.417.6379.9144.4 (s)koru
coins84.8 (s)85.091.962.788.633.2 (p)bend
collatz3.6 (p)12.314.641.115.145.3 (p)koru
coprime72.3 (s)71.172.1119.674.862.1 (p)bend
digitsum2.5 (p)6.06.240.06.534.5 (s)koru
exprtree3.1 (s)5.511.116.56.025.6 (s)koru
factorial6.7 (p)40.141.463.540.4491.8 (p)koru
fib27.3 (s)27.427.663.327.529.2 (p)clang*
gcdsum15.3 (p)87.088.7121.887.163.6 (p)koru
hanoi52.2 (s)68.068.284.268.278.3 (s)koru
isqrt3.5 (p)14.713.554.016.630.2 (p)koru
josephus36.5 (s)35.635.552.836.355.5 (s)rust
listops1.6 (s)1.41.817.04.519.2 (s)c
mutual3.9 (p)16.917.240.716.121.7 (p)koru
nestedloop53.3 (s)53.154.275.153.441.5 (p)bend
pascal33.8 (s)33.734.087.234.068.2 (p)c
powmod7.6 (p)30.828.670.228.6816.5 (p)koru
primes2.8 (p)8.08.628.58.025.2 (p)koru
tak36.2 (s)43.043.3101.943.250.6 (p)koru
textstats2.5 (s)2.52.524.94.227.5 (p)clang*
wordfreq1.6 (f)1.75.816.620.821.1 (s)koru
geomean9.722.826.448.528.352.1koru

Mean wall-clock ms; lower is better. The geomean row aggregates each column’s own numbers — for koru and bend that is the disclosed best-of composite, so it answers “best spelling a language can bring,” not “one port.” * marks rows where the narrow winner is an omitted column — fib goes to clang -O3 (27.1), textstats likewise (2.4). Koru variant letters: s serial port, p the [parallel]-annotated port, v the owned-value port the hoist can prove (binarytrees is its only row), f the koru-fold telescoped-checksum variant (wordfreq). Bend variant letters: s serial, p bend-par.

The composite wins 14 of 22 rows — up from 8 on the serial port alone, which is what the first post’s table showed. The eight losses are the honest set: coins, coprime, nestedloop go to bend-par’s fork/join, fib and textstats to clang -O3, listops and pascal to C, josephus to rust. And the composite corrects a row the serial table miscredited: mutual belongs to koru-par’s 3.9, not to odin’s 6.4.

Where the new columns land

koru-par wins eight rows outright — collatz, digitsum, factorial, gcdsum, isqrt, powmod, primes, tak — not against serial Koru alone but against every binary in the suite, including the C and clang columns and Osprey on powmod, which it held before. Per-row speedup over the serial port on this board: gcdsum 6.0×, factorial 6.0×, mutual 4.3×, collatz 4.1×, powmod 4.0×, isqrt 3.3×, digitsum 2.9×, primes 2.8×, tak 1.0×, binarytrees 0.9×, fib 0.7×. Fold rows geomean 3.4× over the serial port (n=9); with the fork/join losses counted the 11-row geomean is 2.6×. fib pays the gate at 0.7× — fib(35) is below the crossover, and the annotation refuses to pretend otherwise. And binarytrees loses under [parallel] now — the hoist left the loop a bare fold, so the split’s overhead has nothing to amortize against; the serial owned-value port is simply the winner.

koru-val is not a column — one row carries a port. And it is the row Haskell took. The owned-value binarytrees port spells the tree as a recursive struct with mint-node/free-tree obligations — real allocation pressure, the thing the arena port discloses it does not measure. Its ! each body is make(d: 13) |> check(t) — it never reads the item, and hoistInvariantLoopPrefix can prove it: build once, check once, fold the checksum 1200 times, discharge on | done. 1.9ms against Haskell’s 16.9 — against the arena serial port’s 41.1 and koru-par’s 12.2 on the earlier arena spelling (2.2 on the owned-value port — splitting a bare fold buys nothing) — oracle-green at 19659600.

The honest label on that win: it is GHC’s move, not a faster allocator. What Koru adds is that the legality is decidable from the spelling — the effect system knows make is clean, knows the <!owned> consume of t must wait for | done, and refuses the hoist outright when the prefix touches the item or reaches an effect.

one kernel three ways — serial arena, parallel arena, hoisted owned-value — over the per-row koru-par speedups, losses drawn

What the board found in the compiler

Same yield as the first run — the suite caught two defects while the columns went in:

  • An unimported qualified call (std/io:print.ln with no import std/io) never resolved its declaration; the emitter name-mangled it anyway and the string arg arrived as a record literal — .@"{{…}}" = "{{…}}" — dying at the Zig stage with a nonsense error. Now a post-canonicalize pass refuses it as KORU040 with the import spelled out.
  • The hoist itself nearly laundered an obligation violation: take { h: *H<!owned> } -> *H<owned!> returns owned!, which read as a mint — but a move of an outer-scope binding is not a mint. Hoisting it relocated the consume out of the loop and quietly legalized the refusal 330_082 pins. Now every <!owned> argument on a lifted step must be bound by an earlier lifted step; consumers of outer obligations stay put.

Measured scope

Both new columns are oracle-green and regression-pinned. This board is arm64 M2 Pro and stays a separate lane from the dedicated x86_64 numbers; nothing here is merged across machines. koru-val ports are .kz — the owned-value spelling needs host declarations for mint/dispose, which is the point: the benchmark’s nominal workload is allocation, and this port does it for real.