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 | Rust (wasm) |
|---|---|---|---|---|---|---|---|---|---|---|
| ackermann | 914.6 ms | 864.2 ms | 4.949 s | 128.9 ms | 130.7 ms | 130.4 ms | 184.7 ms | 110.8 ms | 64.2 ms | 144.9 ms |
| binarytrees | 1.076 s | 1.301 s | 4.806 s | 726.8 ms | 356.2 ms | 127.3 ms | 124.7 ms | 52.3 ms | 16.8 ms | 810.4 ms |
| coins | 611.4 ms | 573.6 ms | 3.444 s | 82.6 ms | 81.0 ms | 101.6 ms | 122.3 ms | 100.5 ms | 53.9 ms | 110.8 ms |
| collatz | 480.2 ms | 463.9 ms | 2.669 s | 13.0 ms | 10.4 ms | 58.9 ms | 67.5 ms | 60.1 ms | 39.3 ms | 24.6 ms |
| coprime | 390.1 ms | 373.1 ms | 2.289 s | 65.7 ms | 63.7 ms | 78.2 ms | 113.2 ms | 90.7 ms | 105.7 ms | 77.3 ms |
| digitsum | 408.3 ms | 397.7 ms | 2.458 s | 5.7 ms | 5.6 ms | 16.4 ms | 29.2 ms | 18.7 ms | 32.3 ms | 19.1 ms |
| exprtree | 44.4 ms | 47.9 ms | 281.2 ms | 10.8 ms | 6.3 ms | 6.5 ms | 14.3 ms | 5.3 ms | 18.1 ms | 20.6 ms |
| factorial | 237.2 ms | 224.5 ms | 1.421 s | 35.5 ms | 35.4 ms | 37.2 ms | 51.7 ms | 48.7 ms | 57.4 ms | 48.6 ms |
| fib | 555.6 ms | 536.0 ms | 3.171 s | 17.6 ms | 18.4 ms | 36.6 ms | 52.2 ms | 24.2 ms | 48.0 ms | 37.3 ms |
| gcdsum | 331.1 ms | 323.4 ms | 1.996 s | 79.5 ms | 79.5 ms | 86.2 ms | 110.5 ms | 101.9 ms | 106.5 ms | 92.1 ms |
| hanoi | 1.254 s | 1.244 s | 7.289 s | 38.9 ms | 41.0 ms | 44.9 ms | 120.1 ms | 62.8 ms | 58.2 ms | 53.1 ms |
| isqrt | 445.2 ms | 444.3 ms | 2.717 s | 11.2 ms | 10.8 ms | 17.5 ms | 34.0 ms | 21.3 ms | 45.3 ms | 23.4 ms |
| josephus | 232.3 ms | 225.6 ms | 1.413 s | 33.5 ms | 33.4 ms | 35.3 ms | 43.2 ms | 41.6 ms | 45.7 ms | 45.3 ms |
| listops | 6.9 ms | 7.4 ms | 24.4 ms | 1.6 ms | 1.4 ms | 3.2 ms | 11.1 ms | 2.2 ms | 16.6 ms | 12.8 ms |
| mutual | 730.0 ms | 685.5 ms | 4.563 s | 13.2 ms | 12.9 ms | 21.6 ms | 329.6 ms | 28.3 ms | 40.0 ms | 30.0 ms |
| nestedloop | 866.9 ms | 862.0 ms | 5.372 s | 46.2 ms | 45.9 ms | 47.8 ms | 61.4 ms | 58.2 ms | 64.4 ms | 62.5 ms |
| pascal | 963.0 ms | 962.7 ms | 5.549 s | 27.8 ms | 28.2 ms | 34.4 ms | 70.6 ms | 50.4 ms | 64.2 ms | 48.1 ms |
| powmod | 687.1 ms | 679.4 ms | 4.332 s | 23.9 ms | 23.8 ms | 34.5 ms | 51.3 ms | 61.4 ms | 60.3 ms | 34.6 ms |
| primes | 238.5 ms | 229.8 ms | 1.473 s | 6.9 ms | 6.6 ms | 8.8 ms | 44.9 ms | 9.1 ms | 17.9 ms | 17.3 ms |
| tak | 471.4 ms | 468.8 ms | 2.567 s | 33.2 ms | 33.0 ms | 35.7 ms | 82.6 ms | 45.1 ms | 68.5 ms | 99.3 ms |
| textstats | 23.8 ms | 24.8 ms | 123.2 ms | 2.0 ms | 2.1 ms | 3.7 ms | 11.8 ms | 4.0 ms | 17.8 ms | 12.8 ms |
| wordfreq | 40.9 ms | 47.8 ms | 187.4 ms | 4.5 ms | 1.5 ms | 3.3 ms | 11.5 ms | 2.6 ms | 17.8 ms | 15.7 ms |
Peak memory
| Benchmark | Osprey | Osprey (ARC) | Osprey (GC) | Rust | C | C# (AOT) | Dart (AOT) | OCaml | Haskell | Rust (wasm) |
|---|---|---|---|---|---|---|---|---|---|---|
| ackermann | 2056.0 MiB | 1.9 MiB | 19.0 MiB | 1.8 MiB | 1.6 MiB | 7.0 MiB | 14.0 MiB | 2.6 MiB | 15.1 MiB | — |
| binarytrees | 2410.8 MiB | 2.8 MiB | 18.2 MiB | 2.1 MiB | 1.7 MiB | 16.1 MiB | 22.5 MiB | 5.1 MiB | 11.0 MiB | — |
| coins | 1371.3 MiB | 1.5 MiB | 19.1 MiB | 1.5 MiB | 1.4 MiB | 6.8 MiB | 13.9 MiB | 2.2 MiB | 11.1 MiB | — |
| collatz | 1102.2 MiB | 1.5 MiB | 20.6 MiB | 1.5 MiB | 1.4 MiB | 6.8 MiB | 13.9 MiB | 2.2 MiB | 11.1 MiB | — |
| coprime | 992.1 MiB | 1.5 MiB | 18.6 MiB | 1.5 MiB | 1.4 MiB | 6.8 MiB | 13.9 MiB | 2.2 MiB | 11.1 MiB | — |
| digitsum | 1063.6 MiB | 1.5 MiB | 18.8 MiB | 1.5 MiB | 1.4 MiB | 6.8 MiB | 13.9 MiB | 2.2 MiB | 11.1 MiB | — |
| exprtree | 109.4 MiB | 3.4 MiB | 18.5 MiB | 2.6 MiB | 11.4 MiB | 15.2 MiB | 22.5 MiB | 6.0 MiB | 11.3 MiB | — |
| factorial | 614.2 MiB | 1.5 MiB | 19.6 MiB | 1.5 MiB | 1.4 MiB | 6.8 MiB | 13.8 MiB | 2.2 MiB | 11.1 MiB | — |
| fib | 1373.8 MiB | 1.5 MiB | 18.6 MiB | 1.5 MiB | 1.4 MiB | 6.8 MiB | 13.9 MiB | 2.2 MiB | 11.1 MiB | — |
| gcdsum | 870.0 MiB | 1.5 MiB | 18.8 MiB | 1.5 MiB | 1.4 MiB | 6.8 MiB | 13.8 MiB | 2.2 MiB | 11.1 MiB | — |
| hanoi | 3085.5 MiB | 1.5 MiB | 20.6 MiB | 1.5 MiB | 1.4 MiB | 6.8 MiB | 14.0 MiB | 2.2 MiB | 11.1 MiB | — |
| isqrt | 1198.5 MiB | 1.5 MiB | 19.1 MiB | 1.5 MiB | 1.4 MiB | 6.8 MiB | 13.9 MiB | 2.2 MiB | 11.1 MiB | — |
| josephus | 614.2 MiB | 1.5 MiB | 18.6 MiB | 1.5 MiB | 1.4 MiB | 6.8 MiB | 13.9 MiB | 2.2 MiB | 11.1 MiB | — |
| listops | 22.2 MiB | 2.0 MiB | 14.0 MiB | 1.6 MiB | 1.4 MiB | 7.0 MiB | 14.3 MiB | 2.5 MiB | 11.3 MiB | — |
| mutual | 1996.9 MiB | 1.5 MiB | 18.6 MiB | 1.5 MiB | 1.4 MiB | 6.8 MiB | 13.9 MiB | 2.2 MiB | 11.1 MiB | — |
| nestedloop | 2397.0 MiB | 1.5 MiB | 18.6 MiB | 1.5 MiB | 1.4 MiB | 6.8 MiB | 13.9 MiB | 2.2 MiB | 11.0 MiB | — |
| pascal | 2459.6 MiB | 1.5 MiB | 18.6 MiB | 1.5 MiB | 1.4 MiB | 6.8 MiB | 13.9 MiB | 2.2 MiB | 11.1 MiB | — |
| powmod | 1901.0 MiB | 1.5 MiB | 18.6 MiB | 1.5 MiB | 1.4 MiB | 6.8 MiB | 14.0 MiB | 2.2 MiB | 11.1 MiB | — |
| primes | 655.8 MiB | 1.5 MiB | 18.5 MiB | 1.5 MiB | 1.4 MiB | 6.8 MiB | 13.9 MiB | 2.2 MiB | 11.1 MiB | — |
| tak | 1162.1 MiB | 1.5 MiB | 18.8 MiB | 1.5 MiB | 1.4 MiB | 6.8 MiB | 13.9 MiB | 2.2 MiB | 11.1 MiB | — |
| textstats | 56.7 MiB | 1.5 MiB | 14.8 MiB | 1.6 MiB | 1.4 MiB | 6.8 MiB | 14.0 MiB | 2.3 MiB | 11.4 MiB | — |
| wordfreq | 102.0 MiB | 1.6 MiB | 18.1 MiB | 1.6 MiB | 1.4 MiB | 6.8 MiB | 14.0 MiB | 2.3 MiB | 11.3 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. Numbers collected during Osprey's superseded plain-arithmetic experiment are historical and must be regenerated before supporting current claims.
- 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. The exact set of outright wins shifts run-to-run because Osprey, Rust, and C now sit within measurement noise of one another.
Where the gap remains
The checked-arithmetic change invalidates the old attribution of CPU and memory
gaps to / and % alone. Integer + - * now produce the same explicit Result
shape, so arithmetic-heavy rows include that safety cost too. Re-run
make bench before drawing current per-case conclusions from this historical
table.
That 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.5 MB —
matching C throughout, beating it on exprtree — with no change to a line of
Osprey source and, on the allocation-heavy cases, slightly faster wall clock.
--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.