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, 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 primes. Lower is better; ★ marks an Osprey win.
CPU time
| Benchmark | Osprey | Osprey (GC) | Rust | C | OCaml | Haskell | Osprey (wasm) | Rust (wasm) |
|---|---|---|---|---|---|---|---|---|
| ackermann | 127.4 ms | 126.6 ms | 128.6 ms | 126.3 ms | 110.8 ms | 65.1 ms | 130.6 ms | 136.1 ms |
| binarytrees | 417.1 ms | 1.498 s | 711.9 ms | 348.2 ms | 50.2 ms | 17.0 ms | 516.0 ms | 793.0 ms |
| coins | 72.1 ms | 70.2 ms | 73.9 ms | 69.0 ms | 91.4 ms | 52.2 ms | 92.1 ms | 100.5 ms |
| collatz | 12.5 ms | 11.8 ms | 11.2 ms | 9.1 ms | 53.8 ms | 35.0 ms | 19.2 ms | 22.6 ms |
| coprime | 62.9 ms | 61.3 ms | 59.1 ms | 58.3 ms | 86.9 ms | 100.1 ms | 69.4 ms | 71.3 ms |
| digitsum | 5.2 ms | 5.1 ms | 5.3 ms | 5.2 ms | 18.5 ms | 29.7 ms | 12.3 ms | 17.3 ms |
| exprtree | 6.7 ms | 20.6 ms | 10.2 ms | 5.7 ms | 5.2 ms | 16.6 ms | — | 19.8 ms |
| factorial | 33.9 ms | 31.7 ms | 32.0 ms | 32.0 ms | 44.0 ms | 52.5 ms | 42.0 ms | 45.8 ms |
| fib | 20.5 ms | 20.5 ms | 16.2 ms | 17.8 ms | 23.8 ms | 45.1 ms | 32.8 ms | 36.6 ms |
| gcdsum | 75.3 ms | 71.7 ms | 71.2 ms | 71.7 ms | 96.9 ms | 100.2 ms | 79.1 ms | 82.7 ms |
| hanoi | 39.0 ms | 36.3 ms | 36.4 ms | 36.1 ms | 59.9 ms | 53.5 ms | 47.5 ms | 51.1 ms |
| isqrt | 12.6 ms | 12.2 ms | 10.1 ms | 9.6 ms | 19.1 ms | 40.6 ms | 23.7 ms | 21.3 ms |
| josephus | 32.1 ms | 30.9 ms | 30.2 ms | 30.3 ms | 37.8 ms | 43.0 ms | 36.9 ms | 40.5 ms |
| listops | 499.2 ms | 651.0 ms | 1.5 ms | 1.3 ms | 2.0 ms | 16.7 ms | — | 13.1 ms |
| mutual | 12.6 ms | 11.9 ms | 12.0 ms | 11.7 ms | 28.6 ms | 34.8 ms | 23.7 ms | 28.1 ms |
| nestedloop | 43.6 ms | 41.4 ms | 43.2 ms | 42.0 ms | 52.3 ms | 63.6 ms | 54.3 ms | 58.0 ms |
| pascal | 27.6 ms | 26.6 ms | 27.0 ms | 26.9 ms | 43.5 ms | 57.8 ms | 42.4 ms | 47.0 ms |
| powmod | 22.6 ms | 21.5 ms | 21.8 ms | 21.7 ms | 56.3 ms | 55.3 ms | 28.4 ms | 31.7 ms |
| primes | 6.3 ms ★ | 6.4 ms | 7.1 ms | 6.7 ms | 8.7 ms | 15.8 ms | 12.5 ms | 16.3 ms |
| tak | 32.6 ms | 32.4 ms | 32.2 ms | 32.7 ms | 43.8 ms | 63.2 ms | 85.4 ms | 91.1 ms |
| textstats | 3.2 ms | 3.4 ms | 2.0 ms | 2.1 ms | 4.0 ms | 17.7 ms | — | 12.2 ms |
| wordfreq | 19.3 ms | 53.3 ms | 4.4 ms | 1.5 ms | 2.4 ms | 18.4 ms | — | 15.3 ms |
Peak memory
| Benchmark | Osprey | Osprey (GC) | Rust | C | OCaml | Haskell | Osprey (wasm) | Rust (wasm) |
|---|---|---|---|---|---|---|---|---|
| ackermann | 1.6 MiB ★ | 1.7 MiB | 1.8 MiB | 1.6 MiB | 2.6 MiB | 15.1 MiB | — | — |
| binarytrees | 905.0 MiB | 12.3 MiB | 2.1 MiB | 1.7 MiB | 5.1 MiB | 11.0 MiB | — | — |
| coins | 1.4 MiB | 1.5 MiB | 1.5 MiB | 1.4 MiB | 2.2 MiB | 11.1 MiB | — | — |
| collatz | 1.4 MiB | 1.4 MiB | 1.5 MiB | 1.4 MiB | 2.2 MiB | 11.1 MiB | — | — |
| coprime | 1.4 MiB | 1.5 MiB | 1.5 MiB | 1.4 MiB | 2.2 MiB | 11.1 MiB | — | — |
| digitsum | 1.4 MiB | 1.4 MiB | 1.5 MiB | 1.4 MiB | 2.2 MiB | 11.1 MiB | — | — |
| exprtree | 9.0 MiB | 6.5 MiB | 2.7 MiB | 11.5 MiB | 6.0 MiB | 11.4 MiB | — | — |
| factorial | 1.4 MiB | 1.5 MiB | 1.5 MiB | 1.4 MiB | 2.2 MiB | 11.1 MiB | — | — |
| fib | 1.4 MiB | 1.4 MiB | 1.5 MiB | 1.4 MiB | 2.2 MiB | 11.1 MiB | — | — |
| gcdsum | 1.4 MiB | 1.5 MiB | 1.5 MiB | 1.4 MiB | 2.2 MiB | 11.1 MiB | — | — |
| hanoi | 1.4 MiB | 1.4 MiB | 1.5 MiB | 1.4 MiB | 2.2 MiB | 11.1 MiB | — | — |
| isqrt | 1.4 MiB | 1.4 MiB | 1.5 MiB | 1.4 MiB | 2.2 MiB | 11.1 MiB | — | — |
| josephus | 1.4 MiB | 1.4 MiB | 1.5 MiB | 1.4 MiB | 2.2 MiB | 11.1 MiB | — | — |
| listops | 2493.5 MiB | 4.1 MiB | 1.6 MiB | 1.4 MiB | 2.5 MiB | 11.3 MiB | — | — |
| mutual | 1.4 MiB | 1.4 MiB | 1.5 MiB | 1.4 MiB | 2.2 MiB | 11.1 MiB | — | — |
| nestedloop | 1.4 MiB | 1.4 MiB | 1.5 MiB | 1.4 MiB | 2.2 MiB | 11.0 MiB | — | — |
| pascal | 1.4 MiB | 1.5 MiB | 1.5 MiB | 1.4 MiB | 2.2 MiB | 11.1 MiB | — | — |
| powmod | 1.4 MiB | 1.5 MiB | 1.5 MiB | 1.4 MiB | 2.2 MiB | 11.1 MiB | — | — |
| primes | 1.4 MiB | 1.5 MiB | 1.5 MiB | 1.4 MiB | 2.2 MiB | 11.1 MiB | — | — |
| tak | 1.4 MiB | 1.4 MiB | 1.5 MiB | 1.4 MiB | 2.2 MiB | 11.1 MiB | — | — |
| textstats | 1.4 MiB | 1.5 MiB | 1.6 MiB | 1.4 MiB | 2.3 MiB | 11.4 MiB | — | — |
| wordfreq | 52.8 MiB | 4.4 MiB | 1.6 MiB | 1.4 MiB | 2.3 MiB | 11.3 MiB | — | — |
Methodology
Every benchmark is implemented identically in all five languages 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 |
| 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. - Osprey does checked arithmetic on every
+ - * %(each returnsResult<int, MathError>, overflow-checked). The others do not by default — we even pass-C overflow-checks=offto Rust to match its release profile. Part of any Osprey gap is the cost of that safety, a real language semantic. - 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: memory
On compute, Osprey is at parity with C and Rust and ahead of OCaml and Haskell.
Peak memory matches C on every case except binarytrees. That benchmark
builds, holds, and checksums millions of small heap nodes — they genuinely
escape, so the optimizer cannot statically free them, and Osprey's default
allocator does not reclaim memory during a run yet.
This is the contract of the Memory Management spec: allocation funnels through one swappable backend boundary, so a reclaiming manager (reference counting, a tracing collector, or an arena) can be linked in to close this last gap without changing a line of Osprey source.
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.