Benchmarks
Osprey compiles through LLVM to a native binary, so the fair question is how it sits against other native-compiled languages. This page measures CPU time and peak memory against Rust, C, C#, Dart, OCaml, and Haskell on classic compute benchmarks — the same naive algorithm, the same parameters, in every language.
The tables below are generated mechanically from the benchmark harness output
by benchmarks/report.py
— never hand-edited. The Osprey column is highlighted; the fastest cell in each
row is emphasised, and ★ marks a benchmark Osprey wins outright (strictly
faster, or lighter, than every other language).
Osprey is the fastest of all five languages on —. Lower is better; ★ marks an Osprey win.
CPU time
| Benchmark | Osprey | Osprey (ARC) | Osprey (GC) | Rust | C | C# (AOT) | Dart (AOT) | OCaml | Haskell | Osprey (wasm) | Rust (wasm) |
|---|---|---|---|---|---|---|---|---|---|---|---|
| ackermann | 1.382 s | 860.8 ms | 5.514 s | 129.3 ms | 130.6 ms | 132.4 ms | 185.3 ms | 111.3 ms | 65.3 ms | wrong✗ | 145.5 ms |
| binarytrees | 1.224 s | 1.156 s | 5.346 s | 743.8 ms | 362.9 ms | 128.4 ms | 128.6 ms | 52.3 ms | 17.0 ms | 1.165 s | 818.6 ms |
| coins | 660.2 ms | 416.7 ms | 2.582 s | 84.5 ms | 79.8 ms | 104.4 ms | 126.0 ms | 103.3 ms | 52.3 ms | 466.5 ms | 114.0 ms |
| collatz | 516.9 ms | 343.7 ms | 2.162 s | 13.4 ms | 10.8 ms | 59.3 ms | 72.7 ms | 64.6 ms | 40.2 ms | 380.3 ms | 25.4 ms |
| coprime | 189.7 ms | 148.4 ms | 703.3 ms | 66.4 ms | 65.0 ms | 80.5 ms | 111.6 ms | 119.4 ms | 220.3 ms | 296.5 ms | 163.3 ms |
| digitsum | 686.9 ms | 397.9 ms | 4.629 s | 6.1 ms | 6.1 ms | 21.2 ms | 32.8 ms | 19.5 ms | 29.4 ms | 280.0 ms | 18.8 ms |
| exprtree | 46.4 ms | 28.1 ms | 148.9 ms | 11.1 ms | 6.1 ms | 7.7 ms | 15.8 ms | 5.1 ms | 16.3 ms | 33.7 ms | 20.4 ms |
| factorial | 35.8 ms | 35.5 ms | 35.4 ms | 35.6 ms | 35.3 ms | 37.4 ms | 51.2 ms | 48.7 ms | 53.5 ms | 44.7 ms | 48.6 ms |
| fib | 1.097 s | 600.5 ms | 4.502 s | 22.7 ms | 23.4 ms | 43.3 ms | 59.8 ms | 29.5 ms | 56.5 ms | 684.9 ms | 49.4 ms |
| gcdsum | 133.9 ms | 120.1 ms | 241.2 ms | 86.1 ms | 86.5 ms | 92.8 ms | 122.0 ms | 107.7 ms | 115.0 ms | 126.5 ms | 104.3 ms |
| hanoi | 1.766 s | 979.3 ms | 5.488 s | 37.5 ms | 38.5 ms | 38.7 ms | 103.9 ms | 62.9 ms | 54.4 ms | 766.1 ms | 52.3 ms |
| isqrt | 563.8 ms | 414.3 ms | 3.009 s | 10.9 ms | 10.6 ms | 17.1 ms | 32.5 ms | 20.6 ms | 41.0 ms | 412.9 ms | 22.9 ms |
| josephus | 380.1 ms | 222.2 ms | 1.513 s | 33.8 ms | 33.4 ms | 35.9 ms | 42.0 ms | 42.0 ms | 44.5 ms | 243.7 ms | 44.4 ms |
| listops | 7.2 ms | 6.1 ms | 17.4 ms | 1.5 ms | 1.3 ms | 3.3 ms | 10.8 ms | 2.2 ms | 16.6 ms | 13.4 ms | 12.5 ms |
| mutual | 19.3 ms | 18.3 ms | 19.7 ms | 13.1 ms | 12.9 ms | 21.5 ms | 335.0 ms | 29.1 ms | 40.7 ms | 40.4 ms | 30.0 ms |
| nestedloop | 870.6 ms | 503.7 ms | 3.463 s | 46.4 ms | 46.0 ms | 48.4 ms | 60.6 ms | 58.5 ms | 65.2 ms | 381.6 ms | 62.9 ms |
| pascal | 1.602 s | 936.7 ms | 6.487 s | 27.9 ms | 28.2 ms | 34.1 ms | 65.2 ms | 45.1 ms | 65.0 ms | 970.5 ms | 46.9 ms |
| powmod | 883.9 ms | 498.5 ms | 4.019 s | 27.4 ms | 27.5 ms | 40.1 ms | 58.8 ms | 71.1 ms | 65.4 ms | 407.8 ms | 39.5 ms |
| primes | 316.4 ms | 171.6 ms | 1.257 s | 7.1 ms | 7.1 ms | 10.7 ms | 52.6 ms | 10.0 ms | 18.7 ms | 144.4 ms | 21.0 ms |
| tak | 598.0 ms | 363.7 ms | 2.673 s | 37.5 ms | 37.3 ms | 40.5 ms | 93.3 ms | 51.2 ms | 75.0 ms | 480.5 ms | 92.0 ms |
| textstats | 23.3 ms | 16.0 ms | 75.1 ms | 2.0 ms | 2.2 ms | 3.8 ms | 11.4 ms | 4.0 ms | 16.7 ms | 25.2 ms | 14.9 ms |
| wordfreq | 37.5 ms | 40.3 ms | 141.7 ms | 4.5 ms | 1.5 ms | 3.3 ms | 10.6 ms | 2.5 ms | 16.4 ms | 45.7 ms | 15.7 ms |
Peak memory
| Benchmark | Osprey | Osprey (ARC) | Osprey (GC) | Rust | C | C# (AOT) | Dart (AOT) | OCaml | Haskell | Osprey (wasm) | Rust (wasm) |
|---|---|---|---|---|---|---|---|---|---|---|---|
| ackermann | 2.0 MiB | 2.0 MiB | 21.0 MiB | 1.8 MiB | 1.7 MiB | 7.1 MiB | 14.0 MiB | 2.7 MiB | 15.1 MiB | wrong✗ | — |
| binarytrees | 1808.5 MiB | 2.9 MiB | 20.3 MiB | 2.2 MiB | 1.8 MiB | 16.1 MiB | 22.5 MiB | 5.2 MiB | 11.1 MiB | — | — |
| coins | 1.5 MiB | 1.5 MiB | 20.5 MiB | 1.6 MiB | 1.5 MiB | 6.8 MiB | 13.9 MiB | 2.3 MiB | 11.1 MiB | — | — |
| collatz | 221.8 MiB | 1.5 MiB | 18.5 MiB | 1.6 MiB | 1.4 MiB | 6.9 MiB | 14.0 MiB | 2.3 MiB | 11.1 MiB | — | — |
| coprime | 1.5 MiB | 1.5 MiB | 18.7 MiB | 1.6 MiB | 1.4 MiB | 6.8 MiB | 14.0 MiB | 2.3 MiB | 11.1 MiB | — | — |
| digitsum | 335.1 MiB | 1.5 MiB | 22.0 MiB | 1.6 MiB | 1.4 MiB | 6.9 MiB | 14.0 MiB | 2.3 MiB | 11.1 MiB | — | — |
| exprtree | 9.1 MiB | 3.4 MiB | 18.5 MiB | 2.7 MiB | 11.5 MiB | 15.2 MiB | 22.5 MiB | 6.1 MiB | 11.4 MiB | — | — |
| factorial | 1.5 MiB | 1.5 MiB | 1.5 MiB | 1.6 MiB | 1.4 MiB | 6.8 MiB | 13.9 MiB | 2.3 MiB | 11.1 MiB | — | — |
| fib | 1.5 MiB | 1.5 MiB | 22.4 MiB | 1.6 MiB | 1.4 MiB | 6.8 MiB | 13.9 MiB | 2.2 MiB | 11.2 MiB | — | — |
| gcdsum | 1.5 MiB | 1.5 MiB | 22.3 MiB | 1.6 MiB | 1.4 MiB | 6.9 MiB | 14.0 MiB | 2.3 MiB | 11.1 MiB | — | — |
| hanoi | 1.5 MiB | 1.5 MiB | 22.4 MiB | 1.6 MiB | 1.4 MiB | 6.8 MiB | 13.8 MiB | 2.3 MiB | 11.2 MiB | — | — |
| isqrt | 779.1 MiB | 1.5 MiB | 20.7 MiB | 1.6 MiB | 1.4 MiB | 6.8 MiB | 13.9 MiB | 2.3 MiB | 11.1 MiB | — | — |
| josephus | 1.5 MiB | 1.5 MiB | 18.6 MiB | 1.6 MiB | 1.4 MiB | 6.8 MiB | 13.8 MiB | 2.3 MiB | 11.1 MiB | — | — |
| listops | 14.4 MiB | 2.1 MiB | 12.9 MiB | 1.7 MiB | 1.5 MiB | 7.1 MiB | 14.3 MiB | 2.6 MiB | 11.4 MiB | — | — |
| mutual | 1.5 MiB | 1.5 MiB | 7.3 MiB | 1.6 MiB | 1.4 MiB | 6.9 MiB | 14.0 MiB | 2.3 MiB | 11.1 MiB | — | — |
| nestedloop | 1.5 MiB | 1.5 MiB | 20.2 MiB | 1.6 MiB | 1.4 MiB | 6.8 MiB | 13.9 MiB | 2.2 MiB | 11.1 MiB | — | — |
| pascal | 1.5 MiB | 1.5 MiB | 18.6 MiB | 1.6 MiB | 1.4 MiB | 6.9 MiB | 13.9 MiB | 2.2 MiB | 11.1 MiB | — | — |
| powmod | 1.5 MiB | 1.5 MiB | 20.7 MiB | 1.6 MiB | 1.4 MiB | 6.9 MiB | 14.0 MiB | 2.3 MiB | 11.1 MiB | — | — |
| primes | 1.5 MiB | 1.5 MiB | 19.6 MiB | 1.6 MiB | 1.4 MiB | 6.9 MiB | 14.0 MiB | 2.2 MiB | 11.1 MiB | — | — |
| tak | 1.5 MiB | 1.5 MiB | 20.5 MiB | 1.6 MiB | 1.4 MiB | 6.9 MiB | 13.9 MiB | 2.3 MiB | 11.2 MiB | — | — |
| textstats | 1.5 MiB | 1.6 MiB | 18.6 MiB | 1.6 MiB | 1.4 MiB | 6.8 MiB | 14.0 MiB | 2.3 MiB | 11.4 MiB | — | — |
| wordfreq | 59.1 MiB | 1.6 MiB | 18.0 MiB | 1.6 MiB | 1.4 MiB | 6.8 MiB | 14.0 MiB | 2.3 MiB | 11.4 MiB | — | — |
Methodology
Every benchmark is implemented identically in every language under
benchmarks/cases/<name>/,
compiled to a native binary, checked for correct output, then timed.
- Build once, time the binary.
osprey … --compileemits a persistent native executable; we time that, never--run(which would fold compile and link into the measurement). Every language uses its standard optimizing release flags. - Correctness oracle. Each binary runs once and its output is compared to the
case's
expected.txt. A mismatch or build failure is excluded from timing — we never publish a number for a program that computed the wrong thing. Every case has a single deterministic integer result, so output is byte-comparable across languages. - CPU.
hyperfine-N --warmup 3 --min-runs 10per case → statistical mean ± standard deviation. - Memory.
/usr/bin/timepeak resident set size (-lon macOS,-von Linux), max over a few runs.
Compile commands
| Language | Command |
|---|---|
| Osprey | osprey <f>.osp --compile (LLVM IR → clang -O2; override with OSPREY_OPT) |
| Rust | rustc -C opt-level=3 -C overflow-checks=off |
| C | cc -O2 |
| C# | dotnet publish -c Release (AOT) |
| Dart | dart compile exe |
| OCaml | ocamlopt -O3 -unsafe |
| Haskell | ghc -O2 |
Reading the numbers fairly
- Same algorithm everywhere. Identical naive algorithm and parameters in
every language — no memoization, closed forms, SIMD, or parallelism. We measure
the language/compiler/runtime, not who is cleverest. Ranges match Osprey's
half-open
range(a, b)=[a, b)exactly. - Integer arithmetic is checked. Osprey's integer
+ - *, unary-, andabsreturnResult<int, MathError>and report overflow; they never silently wrap or panic./and%likewise preserve their failure channel. Programs must handle the Result withmatch/?:, or propagate it through arithmetic.checkedAdd/checkedSub/checkedMulremain safe compatibility aliases, not an opt-in safety tier. See ARITH-CHECKED. - The Rust command disables Rust's overflow checks. The comparison is deliberately asymmetric: Osprey enforces its checked arithmetic contract while this Rust configuration measures wrapping release arithmetic. Every number on this page was measured under that contract.
- Osprey loops via
range |> fold, not deep linear recursion, because it has no tail-call optimization yet (a 1e6-deep recursion overflows the stack). The work is identical; only the iteration mechanism differs. - OCaml is built without flambda (stock
ocamlopt), so its numbers are conservative versus an flambda build. - Single machine, wall clock. Treat ratios as indicative; re-run locally with
make bench.
Where the gap remains
Osprey is not the fastest language in this table on any case. Averaged across
the suite it runs 11.6× Rust's CPU time and 13.1× C's, and the default
memory backend never wins a row either — binarytrees peaks at 1.77 GB
against C's 1.75 MB.
The CPU gap is not attributable to / and % alone. Under
ARITH-CHECKED integer + - * also produce an
explicit Result<int, MathError>, so every arithmetic-heavy row carries that
safety cost: fn addup(a, b) = a + b returns a heap-allocated Result, not an
i64. Making that representation cheap is open work; removing its failure
channel is not.
The memory gap is a backend choice, not a language one. Allocation funnels
through the one swappable boundary of the
Memory Management spec, and under
--memory=arc (Perceus reference counting) every case drops to 1.5–3.4 MB,
with no change to a line of Osprey source. That lands ARC within striking
distance of C — a median of 1.06× C's peak RSS, from 0.30× on exprtree
(where it beats C outright) to 1.65× on binarytrees — turning a 1000×
memory deficit into a rounding error.
Wall clock cuts both ways, and the split is the opposite of what you might
expect. On the 17 allocation-light cases ARC is slightly faster than the
non-reclaiming default (up to 6% on ackermann, coins, mutual), because
freeing early keeps the working set in cache. On the 5 allocation-heavy cases it
is slower — binarytrees by 21%, wordfreq by 17%, exprtree and listops
by 8% — which is the refcount traffic those cases exist to provoke. Reclaiming
memory is a real trade, not a free win. --memory=gc offers the same trade with
a tracing collector.
Reproduce it
make bench # build everything, run the whole suite
BENCH_FILTER=fib make bench # only cases whose name contains "fib"
Results land in benchmarks/results/ — results.html (this report, standalone),
results.json (structured), and the per-case hyperfine exports.