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)
11.59×CPU vs Rust
13.14×CPU vs C
9.19×CPU vs C# (AOT)
4.69×CPU vs Dart (AOT)
8.91×CPU vs OCaml
6.25×CPU vs Haskell
5.65×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)OCamlHaskellOsprey (wasm)Rust (wasm)
ackermann1.382 s860.8 ms5.514 s129.3 ms130.6 ms132.4 ms185.3 ms111.3 ms65.3 mswrong✗145.5 ms
binarytrees1.224 s1.156 s5.346 s743.8 ms362.9 ms128.4 ms128.6 ms52.3 ms17.0 ms1.165 s818.6 ms
coins660.2 ms416.7 ms2.582 s84.5 ms79.8 ms104.4 ms126.0 ms103.3 ms52.3 ms466.5 ms114.0 ms
collatz516.9 ms343.7 ms2.162 s13.4 ms10.8 ms59.3 ms72.7 ms64.6 ms40.2 ms380.3 ms25.4 ms
coprime189.7 ms148.4 ms703.3 ms66.4 ms65.0 ms80.5 ms111.6 ms119.4 ms220.3 ms296.5 ms163.3 ms
digitsum686.9 ms397.9 ms4.629 s6.1 ms6.1 ms21.2 ms32.8 ms19.5 ms29.4 ms280.0 ms18.8 ms
exprtree46.4 ms28.1 ms148.9 ms11.1 ms6.1 ms7.7 ms15.8 ms5.1 ms16.3 ms33.7 ms20.4 ms
factorial35.8 ms35.5 ms35.4 ms35.6 ms35.3 ms37.4 ms51.2 ms48.7 ms53.5 ms44.7 ms48.6 ms
fib1.097 s600.5 ms4.502 s22.7 ms23.4 ms43.3 ms59.8 ms29.5 ms56.5 ms684.9 ms49.4 ms
gcdsum133.9 ms120.1 ms241.2 ms86.1 ms86.5 ms92.8 ms122.0 ms107.7 ms115.0 ms126.5 ms104.3 ms
hanoi1.766 s979.3 ms5.488 s37.5 ms38.5 ms38.7 ms103.9 ms62.9 ms54.4 ms766.1 ms52.3 ms
isqrt563.8 ms414.3 ms3.009 s10.9 ms10.6 ms17.1 ms32.5 ms20.6 ms41.0 ms412.9 ms22.9 ms
josephus380.1 ms222.2 ms1.513 s33.8 ms33.4 ms35.9 ms42.0 ms42.0 ms44.5 ms243.7 ms44.4 ms
listops7.2 ms6.1 ms17.4 ms1.5 ms1.3 ms3.3 ms10.8 ms2.2 ms16.6 ms13.4 ms12.5 ms
mutual19.3 ms18.3 ms19.7 ms13.1 ms12.9 ms21.5 ms335.0 ms29.1 ms40.7 ms40.4 ms30.0 ms
nestedloop870.6 ms503.7 ms3.463 s46.4 ms46.0 ms48.4 ms60.6 ms58.5 ms65.2 ms381.6 ms62.9 ms
pascal1.602 s936.7 ms6.487 s27.9 ms28.2 ms34.1 ms65.2 ms45.1 ms65.0 ms970.5 ms46.9 ms
powmod883.9 ms498.5 ms4.019 s27.4 ms27.5 ms40.1 ms58.8 ms71.1 ms65.4 ms407.8 ms39.5 ms
primes316.4 ms171.6 ms1.257 s7.1 ms7.1 ms10.7 ms52.6 ms10.0 ms18.7 ms144.4 ms21.0 ms
tak598.0 ms363.7 ms2.673 s37.5 ms37.3 ms40.5 ms93.3 ms51.2 ms75.0 ms480.5 ms92.0 ms
textstats23.3 ms16.0 ms75.1 ms2.0 ms2.2 ms3.8 ms11.4 ms4.0 ms16.7 ms25.2 ms14.9 ms
wordfreq37.5 ms40.3 ms141.7 ms4.5 ms1.5 ms3.3 ms10.6 ms2.5 ms16.4 ms45.7 ms15.7 ms

Peak memory

BenchmarkOspreyOsprey (ARC)Osprey (GC)RustCC# (AOT)Dart (AOT)OCamlHaskellOsprey (wasm)Rust (wasm)
ackermann2.0 MiB2.0 MiB21.0 MiB1.8 MiB1.7 MiB7.1 MiB14.0 MiB2.7 MiB15.1 MiBwrong✗
binarytrees1808.5 MiB2.9 MiB20.3 MiB2.2 MiB1.8 MiB16.1 MiB22.5 MiB5.2 MiB11.1 MiB
coins1.5 MiB1.5 MiB20.5 MiB1.6 MiB1.5 MiB6.8 MiB13.9 MiB2.3 MiB11.1 MiB
collatz221.8 MiB1.5 MiB18.5 MiB1.6 MiB1.4 MiB6.9 MiB14.0 MiB2.3 MiB11.1 MiB
coprime1.5 MiB1.5 MiB18.7 MiB1.6 MiB1.4 MiB6.8 MiB14.0 MiB2.3 MiB11.1 MiB
digitsum335.1 MiB1.5 MiB22.0 MiB1.6 MiB1.4 MiB6.9 MiB14.0 MiB2.3 MiB11.1 MiB
exprtree9.1 MiB3.4 MiB18.5 MiB2.7 MiB11.5 MiB15.2 MiB22.5 MiB6.1 MiB11.4 MiB
factorial1.5 MiB1.5 MiB1.5 MiB1.6 MiB1.4 MiB6.8 MiB13.9 MiB2.3 MiB11.1 MiB
fib1.5 MiB1.5 MiB22.4 MiB1.6 MiB1.4 MiB6.8 MiB13.9 MiB2.2 MiB11.2 MiB
gcdsum1.5 MiB1.5 MiB22.3 MiB1.6 MiB1.4 MiB6.9 MiB14.0 MiB2.3 MiB11.1 MiB
hanoi1.5 MiB1.5 MiB22.4 MiB1.6 MiB1.4 MiB6.8 MiB13.8 MiB2.3 MiB11.2 MiB
isqrt779.1 MiB1.5 MiB20.7 MiB1.6 MiB1.4 MiB6.8 MiB13.9 MiB2.3 MiB11.1 MiB
josephus1.5 MiB1.5 MiB18.6 MiB1.6 MiB1.4 MiB6.8 MiB13.8 MiB2.3 MiB11.1 MiB
listops14.4 MiB2.1 MiB12.9 MiB1.7 MiB1.5 MiB7.1 MiB14.3 MiB2.6 MiB11.4 MiB
mutual1.5 MiB1.5 MiB7.3 MiB1.6 MiB1.4 MiB6.9 MiB14.0 MiB2.3 MiB11.1 MiB
nestedloop1.5 MiB1.5 MiB20.2 MiB1.6 MiB1.4 MiB6.8 MiB13.9 MiB2.2 MiB11.1 MiB
pascal1.5 MiB1.5 MiB18.6 MiB1.6 MiB1.4 MiB6.9 MiB13.9 MiB2.2 MiB11.1 MiB
powmod1.5 MiB1.5 MiB20.7 MiB1.6 MiB1.4 MiB6.9 MiB14.0 MiB2.3 MiB11.1 MiB
primes1.5 MiB1.5 MiB19.6 MiB1.6 MiB1.4 MiB6.9 MiB14.0 MiB2.2 MiB11.1 MiB
tak1.5 MiB1.5 MiB20.5 MiB1.6 MiB1.4 MiB6.9 MiB13.9 MiB2.3 MiB11.2 MiB
textstats1.5 MiB1.6 MiB18.6 MiB1.6 MiB1.4 MiB6.8 MiB14.0 MiB2.3 MiB11.4 MiB
wordfreq59.1 MiB1.6 MiB18.0 MiB1.6 MiB1.4 MiB6.8 MiB14.0 MiB2.3 MiB11.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.

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