Benchmarks

By Christian Findlay

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).

0CPU wins (fastest of all)
13.64×CPU vs Rust
15.43×CPU vs C
11.05×CPU vs C# (AOT)
5.49×CPU vs Dart (AOT)
10.60×CPU vs OCaml
7.35×CPU vs Haskell
6.91×CPU vs Rust (wasm)

Osprey is the fastest of all five languages on . Lower is better; ★ marks an Osprey win.

CPU time

BenchmarkOspreyOsprey (ARC)Osprey (GC)RustCC# (AOT)Dart (AOT)OCamlHaskellRust (wasm)
ackermann914.6 ms864.2 ms4.949 s128.9 ms130.7 ms130.4 ms184.7 ms110.8 ms64.2 ms144.9 ms
binarytrees1.076 s1.301 s4.806 s726.8 ms356.2 ms127.3 ms124.7 ms52.3 ms16.8 ms810.4 ms
coins611.4 ms573.6 ms3.444 s82.6 ms81.0 ms101.6 ms122.3 ms100.5 ms53.9 ms110.8 ms
collatz480.2 ms463.9 ms2.669 s13.0 ms10.4 ms58.9 ms67.5 ms60.1 ms39.3 ms24.6 ms
coprime390.1 ms373.1 ms2.289 s65.7 ms63.7 ms78.2 ms113.2 ms90.7 ms105.7 ms77.3 ms
digitsum408.3 ms397.7 ms2.458 s5.7 ms5.6 ms16.4 ms29.2 ms18.7 ms32.3 ms19.1 ms
exprtree44.4 ms47.9 ms281.2 ms10.8 ms6.3 ms6.5 ms14.3 ms5.3 ms18.1 ms20.6 ms
factorial237.2 ms224.5 ms1.421 s35.5 ms35.4 ms37.2 ms51.7 ms48.7 ms57.4 ms48.6 ms
fib555.6 ms536.0 ms3.171 s17.6 ms18.4 ms36.6 ms52.2 ms24.2 ms48.0 ms37.3 ms
gcdsum331.1 ms323.4 ms1.996 s79.5 ms79.5 ms86.2 ms110.5 ms101.9 ms106.5 ms92.1 ms
hanoi1.254 s1.244 s7.289 s38.9 ms41.0 ms44.9 ms120.1 ms62.8 ms58.2 ms53.1 ms
isqrt445.2 ms444.3 ms2.717 s11.2 ms10.8 ms17.5 ms34.0 ms21.3 ms45.3 ms23.4 ms
josephus232.3 ms225.6 ms1.413 s33.5 ms33.4 ms35.3 ms43.2 ms41.6 ms45.7 ms45.3 ms
listops6.9 ms7.4 ms24.4 ms1.6 ms1.4 ms3.2 ms11.1 ms2.2 ms16.6 ms12.8 ms
mutual730.0 ms685.5 ms4.563 s13.2 ms12.9 ms21.6 ms329.6 ms28.3 ms40.0 ms30.0 ms
nestedloop866.9 ms862.0 ms5.372 s46.2 ms45.9 ms47.8 ms61.4 ms58.2 ms64.4 ms62.5 ms
pascal963.0 ms962.7 ms5.549 s27.8 ms28.2 ms34.4 ms70.6 ms50.4 ms64.2 ms48.1 ms
powmod687.1 ms679.4 ms4.332 s23.9 ms23.8 ms34.5 ms51.3 ms61.4 ms60.3 ms34.6 ms
primes238.5 ms229.8 ms1.473 s6.9 ms6.6 ms8.8 ms44.9 ms9.1 ms17.9 ms17.3 ms
tak471.4 ms468.8 ms2.567 s33.2 ms33.0 ms35.7 ms82.6 ms45.1 ms68.5 ms99.3 ms
textstats23.8 ms24.8 ms123.2 ms2.0 ms2.1 ms3.7 ms11.8 ms4.0 ms17.8 ms12.8 ms
wordfreq40.9 ms47.8 ms187.4 ms4.5 ms1.5 ms3.3 ms11.5 ms2.6 ms17.8 ms15.7 ms

Peak memory

BenchmarkOspreyOsprey (ARC)Osprey (GC)RustCC# (AOT)Dart (AOT)OCamlHaskellRust (wasm)
ackermann2056.0 MiB1.9 MiB19.0 MiB1.8 MiB1.6 MiB7.0 MiB14.0 MiB2.6 MiB15.1 MiB
binarytrees2410.8 MiB2.8 MiB18.2 MiB2.1 MiB1.7 MiB16.1 MiB22.5 MiB5.1 MiB11.0 MiB
coins1371.3 MiB1.5 MiB19.1 MiB1.5 MiB1.4 MiB6.8 MiB13.9 MiB2.2 MiB11.1 MiB
collatz1102.2 MiB1.5 MiB20.6 MiB1.5 MiB1.4 MiB6.8 MiB13.9 MiB2.2 MiB11.1 MiB
coprime992.1 MiB1.5 MiB18.6 MiB1.5 MiB1.4 MiB6.8 MiB13.9 MiB2.2 MiB11.1 MiB
digitsum1063.6 MiB1.5 MiB18.8 MiB1.5 MiB1.4 MiB6.8 MiB13.9 MiB2.2 MiB11.1 MiB
exprtree109.4 MiB3.4 MiB18.5 MiB2.6 MiB11.4 MiB15.2 MiB22.5 MiB6.0 MiB11.3 MiB
factorial614.2 MiB1.5 MiB19.6 MiB1.5 MiB1.4 MiB6.8 MiB13.8 MiB2.2 MiB11.1 MiB
fib1373.8 MiB1.5 MiB18.6 MiB1.5 MiB1.4 MiB6.8 MiB13.9 MiB2.2 MiB11.1 MiB
gcdsum870.0 MiB1.5 MiB18.8 MiB1.5 MiB1.4 MiB6.8 MiB13.8 MiB2.2 MiB11.1 MiB
hanoi3085.5 MiB1.5 MiB20.6 MiB1.5 MiB1.4 MiB6.8 MiB14.0 MiB2.2 MiB11.1 MiB
isqrt1198.5 MiB1.5 MiB19.1 MiB1.5 MiB1.4 MiB6.8 MiB13.9 MiB2.2 MiB11.1 MiB
josephus614.2 MiB1.5 MiB18.6 MiB1.5 MiB1.4 MiB6.8 MiB13.9 MiB2.2 MiB11.1 MiB
listops22.2 MiB2.0 MiB14.0 MiB1.6 MiB1.4 MiB7.0 MiB14.3 MiB2.5 MiB11.3 MiB
mutual1996.9 MiB1.5 MiB18.6 MiB1.5 MiB1.4 MiB6.8 MiB13.9 MiB2.2 MiB11.1 MiB
nestedloop2397.0 MiB1.5 MiB18.6 MiB1.5 MiB1.4 MiB6.8 MiB13.9 MiB2.2 MiB11.0 MiB
pascal2459.6 MiB1.5 MiB18.6 MiB1.5 MiB1.4 MiB6.8 MiB13.9 MiB2.2 MiB11.1 MiB
powmod1901.0 MiB1.5 MiB18.6 MiB1.5 MiB1.4 MiB6.8 MiB14.0 MiB2.2 MiB11.1 MiB
primes655.8 MiB1.5 MiB18.5 MiB1.5 MiB1.4 MiB6.8 MiB13.9 MiB2.2 MiB11.1 MiB
tak1162.1 MiB1.5 MiB18.8 MiB1.5 MiB1.4 MiB6.8 MiB13.9 MiB2.2 MiB11.1 MiB
textstats56.7 MiB1.5 MiB14.8 MiB1.6 MiB1.4 MiB6.8 MiB14.0 MiB2.3 MiB11.4 MiB
wordfreq102.0 MiB1.6 MiB18.1 MiB1.6 MiB1.4 MiB6.8 MiB14.0 MiB2.3 MiB11.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.

  1. Build once, time the binary. osprey … --compile emits 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.
  2. 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.
  3. CPU. hyperfine -N --warmup 3 --min-runs 10 per case → statistical mean ± standard deviation.
  4. Memory. /usr/bin/time peak resident set size (-l on macOS, -v on 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 -, and abs return Result<int, MathError> and report overflow; they never silently wrap or panic. / and % likewise preserve their failure channel. Programs must handle the Result with match/?:, or propagate it through arithmetic. checkedAdd/checkedSub/checkedMul remain 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.