Four small programs, written once per language and checked to print the same bytes, timed on the same idle machine — with the controls that say which of the differences are real.
They were chosen for four different costs. The most useful result on this page is that they do not agree on an ordering.
| Program | Size | What it stresses |
|---|---|---|
fib(35) | 9.2M recursive calls | Call overhead and integer arithmetic. Nothing is allocated, so a language's memory strategy cannot help or hurt it. |
loops | 100M iterations | A while loop, a modulo and an addition. The body compiles to the same handful of instructions in every language that compiles. |
objects | 10M allocations | A two-field record built and dropped inside every iteration. This is where reference counting, tracing collection and stack allocation give different answers. |
lists | 1M elements through map / filter / fold | Each language uses its own list type and its own higher-order functions. It is the least like-for-like of the four, and the column where that matters most. |
Every figure is the minimum of the runs minus that runtime's own hello-world time, so a JVM's boot is not charged to its arithmetic. The minimum rather than the median: a machine under interference produces a long right tail and no left one.
| Language | fib(35) | loops | objects | lists | startup |
|---|---|---|---|---|---|
| C | 9.1 | 43.1 | 10.1 | 3.9 | 0.2 |
| C++ | 9.3 | 43.8 | 10.2 | 5.8 | 0.3 |
| Rust | 16.1 | 40.5 | 9.3 | 3.5 | 0.4 |
| Keal | 20.4 | 44.2 | 10.0 | 13.4 | 0.3 |
| Go | 22.7 | 46.2 | 11.1 | 30.9 | 0.8 |
| Java | 15.3 | 43.9 | 14.2 | 97.3 | 14.9 |
| Kotlin | 13.1 | 42.6 | 13.7 | 78.7 | 23.2 |
| Python | 449.0 | 4 684 | 1 465 | 94.8 | 6.2 |
fib(35) — 9.2M recursive calls
loops — 100M iterations
objects — 10M allocations
lists — 1M elements through map / filter / fold
How far the median sits above the minimum, as a percentage. Below about 10% a figure is settled; well above it, two languages a few percent apart are not ranked by this data, and the page does not pretend otherwise.
| Language | fib(35) | loops | objects | lists |
|---|---|---|---|---|
| C | 9% | 7% | 8% | 19% |
| C++ | 6% | 7% | 7% | 13% |
| Rust | 7% | 8% | 5% | 17% |
| Keal | 7% | 3% | 7% | 4% |
| Go | 3% | 5% | 5% | 89% |
| Java | 6% | 9% | 6% | 11% |
| Kotlin | 11% | 10% | 12% | 8% |
| Python | 8% | 3% | 5% | 7% |
| Language | Version | Build |
|---|---|---|
| C | gcc (Ubuntu 15.2.0-16ubuntu1) 15.2.0 | -O2 -std=c11 |
| C++ | g++ (Ubuntu 15.2.0-16ubuntu1) 15.2.0 | -O2 -std=c++17 |
| Rust | rustc 1.98.0 (88d9e12ae 2026-08-18) | -C opt-level=2 |
| Keal | keal 1.2.0 | keal build |
| Go | go version go1.25.1 linux/arm64 | go build |
| Java | openjdk version "25.0.4" 2026-07-21 | javac, default JVM |
| Kotlin | kotlinc-jvm 2.4.10 (JRE 25.0.4+7-1-26.04-Ubuntu); jars run on openjdk version "25.0.4" 2026-07-21 | -include-runtime, jars run on the PATH java |
| Python | CPython 3.14.4 | stock build |
The JDK and the interpreter above were arranged for this run rather than taken from the machine. Both installs are keg-only: this Mac's default java is 23 and its default python3 is Apple's 3.9.6. Reproducing with the machine's own defaults gives different numbers — CPython 3.9 alone runs fib 2.2× slower than 3.14 here, which is larger than any difference between the machines on that row.
Every figure is the minimum of the runs minus that runtime's own hello-world time, so a JVM's boot is not charged to its arithmetic. The minimum rather than the median: a machine under interference produces a long right tail and no left one.
| Language | fib(35) | loops | objects | lists | startup |
|---|---|---|---|---|---|
| C | 16.4 | 32.5 | 6.0 | 2.5 | 1.8 |
| C++ | 16.4 | 32.8 | 6.0 | 2.9 | 2.0 |
| Rust | 16.2 | 33.4 | 9.1 | 2.9 | 1.9 |
| Keal | 20.9 | 45.4 | 9.5 | 10.4 | 1.7 |
| Go | 20.4 | 46.0 | 9.7 | 7.3 | 2.2 |
| Java | 14.2 | 42.7 | 12.5 | 48.7 | 18.9 |
| Kotlin | 17.0 | 41.8 | 13.0 | 41.5 | 26.3 |
| Python | 605.3 | 5 860 | 1 688 | 106.6 | 15.6 |
fib(35) — 9.2M recursive calls
loops — 100M iterations
objects — 10M allocations
lists — 1M elements through map / filter / fold
How far the median sits above the minimum, as a percentage. Below about 10% a figure is settled; well above it, two languages a few percent apart are not ranked by this data, and the page does not pretend otherwise.
| Language | fib(35) | loops | objects | lists |
|---|---|---|---|---|
| C | 8% | 8% | 13% | 12% |
| C++ | 7% | 11% | 5% | 7% |
| Rust | 7% | 11% | 6% | 16% |
| Keal | 9% | 2% | 5% | 10% |
| Go | 8% | 5% | 12% | 27% |
| Java | 21% | 8% | 8% | 10% |
| Kotlin | 10% | 14% | 7% | 12% |
| Python | 8% | 8% | 11% | 21% |
| Language | Version | Build |
|---|---|---|
| C | Apple clang version 21.0.0 (clang-2100.1.1.101) | -O2 -std=c11 |
| C++ | Apple clang version 21.0.0 (clang-2100.1.1.101) | -O2 -std=c++17 |
| Rust | rustc 1.98.0 (88d9e12ae 2026-08-18) | -C opt-level=2 |
| Keal | keal 1.2.0 | keal build |
| Go | go version go1.27.0 darwin/arm64 | go build |
| Java | openjdk version "25.0.4.1" 2026-08-18 | javac, default JVM |
| Kotlin | kotlinc-jvm 2.4.10 (JRE 25.0.4.1); jars run on openjdk version "25.0.4.1" 2026-08-18 | -include-runtime, jars run on the PATH java |
| Python | CPython 3.14.7 | stock build |
Every toolchain above was installed for this run: the machine had none of them, not even a working Python. Rust is on the GNU ABI rather than MSVC, which is recorded in its flags — C here is MinGW, and an MSVC Rust would have put a second C runtime inside the same column of ratios. keal build takes that same gcc.
Every figure is the minimum of the runs minus that runtime's own hello-world time, so a JVM's boot is not charged to its arithmetic. The minimum rather than the median: a machine under interference produces a long right tail and no left one.
| Language | fib(35) | loops | objects | lists | startup |
|---|---|---|---|---|---|
| C | 11.1 | 92.0 | 19.3 | 7.7 | 4.9 |
| C++ | 11.4 | 91.7 | 19.6 | 10.8 | 5.0 |
| Rust | 21.6 | 66.7 | 20.3 | 6.6 | 5.9 |
| Keal | 45.3 | 108.2 | 18.4 | 17.6 | 6.2 |
| Go | 41.4 | 83.5 | 19.6 | 23.6 | 6.9 |
| Java | 26.5 | 108.8 | 32.4 | 101.7 | 46.9 |
| Kotlin | 24.7 | 108.0 | 33.1 | 85.6 | 63.4 |
| Python | 1 130 | 9 278 | 2 896 | 235.4 | 21.4 |
fib(35) — 9.2M recursive calls
loops — 100M iterations
objects — 10M allocations
lists — 1M elements through map / filter / fold
How far the median sits above the minimum, as a percentage. Below about 10% a figure is settled; well above it, two languages a few percent apart are not ranked by this data, and the page does not pretend otherwise.
| Language | fib(35) | loops | objects | lists |
|---|---|---|---|---|
| C | 6% | 1% | 3% | 9% |
| C++ | 5% | 1% | 4% | 5% |
| Rust | 1% | 2% | 4% | 7% |
| Keal | 1% | 1% | 3% | 4% |
| Go | 2% | 2% | 4% | 19% |
| Java | 5% | 3% | 4% | 5% |
| Kotlin | 5% | 1% | 2% | 3% |
| Python | 1% | 3% | 4% | 2% |
| Language | Version | Build |
|---|---|---|
| C | gcc.EXE (MinGW-W64 x86_64-ucrt-posix-seh, built by Brecht Sanders, r1) 16.2.0 | -O2 -std=c11 |
| C++ | g++.EXE (MinGW-W64 x86_64-ucrt-posix-seh, built by Brecht Sanders, r1) 16.2.0 | -O2 -std=c++17 |
| Rust | rustc 1.98.1 (48a229cea 2026-09-01) | -C opt-level=2, x86_64-pc-windows-gnu |
| Keal | keal 1.2.0 | keal build (gcc, MinGW-w64 UCRT) |
| Go | go version go1.27.1 windows/amd64 | go build |
| Java | openjdk version "25.0.4.1" 2026-08-18 LTS | javac, default JVM |
| Kotlin | kotlinc-jvm 2.4.10 (JRE 25.0.4.1+1-LTS) | -include-runtime, default JVM |
| Python | CPython 3.12.10 | stock build |
Where two machines disagree about a ratio, the disagreement is the result — the number was a property of one box rather than of the language. Read the denominator first: several rows moving together and in the same direction is the signature of the C baseline having changed, not of five languages changing at once.
fib(35)
| Language | Linux · aarch64 | macOS · Apple M4 | Windows · x86_64 |
|---|---|---|---|
| C | 1.0× | 1.0× | 1.0× |
| C++ | 1.0× | 1.0× | 1.0× |
| Rust | 1.8× | 1.0× | 1.9× |
| Keal | 2.2× | 1.3× | 4.1× |
| Go | 2.5× | 1.2× | 3.7× |
| Java | 1.7× | 0.9× | 2.4× |
| Kotlin | 1.4× | 1.0× | 2.2× |
| Python | 49× | 37× | 102× |
loops
| Language | Linux · aarch64 | macOS · Apple M4 | Windows · x86_64 |
|---|---|---|---|
| C | 1.0× | 1.0× | 1.0× |
| C++ | 1.0× | 1.0× | 1.0× |
| Rust | 0.9× | 1.0× | 0.7× |
| Keal | 1.0× | 1.4× | 1.2× |
| Go | 1.1× | 1.4× | 0.9× |
| Java | 1.0× | 1.3× | 1.2× |
| Kotlin | 1.0× | 1.3× | 1.2× |
| Python | 109× | 180× | 101× |
objects
| Language | Linux · aarch64 | macOS · Apple M4 | Windows · x86_64 |
|---|---|---|---|
| C | 1.0× | 1.0× | 1.0× |
| C++ | 1.0× | 1.0× | 1.0× |
| Rust | 0.9× | 1.5× | 1.1× |
| Keal | 1.0× | 1.6× | 1.0× |
| Go | 1.1× | 1.6× | 1.0× |
| Java | 1.4× | 2.1× | 1.7× |
| Kotlin | 1.4× | 2.2× | 1.7× |
| Python | 145× | 281× | 150× |
lists
| Language | Linux · aarch64 | macOS · Apple M4 | Windows · x86_64 |
|---|---|---|---|
| C | 1.0× | 1.0× | 1.0× |
| C++ | 1.5× | 1.2× | 1.4× |
| Rust | 0.9× | 1.2× | 0.9× |
| Keal | 3.4× | 4.2× | 2.3× |
| Go | 7.9× | 2.9× | 3.1× |
| Java | 25× | 19× | 13× |
| Kotlin | 20× | 17× | 11× |
| Python | 24× | 43× | 31× |
A benchmark is an instrument, and an instrument nobody calibrated reports its own defects as findings. Each of these could have come back negative.
Every implementation is run and its last line compared against the Keal reference before any clock starts. A faster program that prints a different number is not a faster program.
Ten times the work must cost ten times the time. Checked by rebuilding at 10× and re-timing rather than by reading the assembly — though the loop's backward branch was confirmed there too. Ten times the work came back at 9.0–9.6× the time.
The whole set is measured twice: once blocked, every replicate of a configuration consecutively, and once interleaved in a shuffled order. The gap between the two designs is then held against that configuration's own spread within a single design. 1 of 96 crossed it — and with a threshold this crude applied 32 times per machine, a marginal crossing or two is what chance alone produces, so the count is reported rather than interpreted.
Rank correlation between when a run happened in the shuffled sequence and how long it took. A machine heating up, or a cache filling, would show as a consistent sign across configurations. The median was 0.19, well under what nine replicates make significant.
The first execution of a binary reads it from disk. Every configuration is run once and thrown away before measurement, so no language pays for being first in the sequence.
The spread table is not decoration. Where a configuration's median sits far above its minimum, its position against a close neighbour is not resolved by this data, and the page names it instead of picking a winner.
lists is not like-for-like. Keal's List<Int>, C++'s vector and Rust's Vec hold unboxed 64-bit integers; Java and Kotlin box every element into a Long. That difference is most of what the JVM pays in that column, and it is a property of the libraries rather than of the compilers.
Each timing is a whole process, run once. No warm-up loop inside the program, no steady-state measurement. This is what running the program costs, which flatters ahead-of-time compilers and understates what a JIT does in a long-lived server.
Four programs are four programs. They were chosen for four different costs, but no set this small predicts a real workload, and none of them touches I/O, strings or concurrency.
The ratio to C carries the C compiler with it. A ratio divides by whatever the local C compiler produced, so it absorbs how fast the hardware is but not the baseline's own optimisation decisions. fib is where that shows. On the Linux machine, gcc at -O2 inlines the recursion several levels deep — 244 instructions in the function body against 30 with inlining turned off — and the same source, same compiler and same flag then runs in 11.8 ms against 20.3 ms. That 1.7× swing is about the size of the gap between the machines' own C baselines on that program. objects is the second such column and it points the other way: against the clang machine, six of seven rows move together by about 1.6× because that C baseline is the fast one there. Between the two gcc machines the same column stays inside the pack, which is what says the effect belongs to the compiler rather than to the hardware. So those two ratios compare within a machine and not across C compilers, and any row that moves between machines has to be read against its denominator before it is read as a fact about the language.
Subtracting startup weighs most on the rows that have the most of it. Each figure has its own machine's hello-world time taken off it, which is what keeps a JVM's boot out of its arithmetic. But a JVM's boot is tens of milliseconds where a native binary's is under two, so the JVM rows carry by far the largest correction — and it moves: on the macOS machine, aligning the JDK changed Java's startup by 8 ms, which is 8 ms on all four of its figures and a tenth of its lists. Comparing a JVM row across machines compares two startup corrections as well as two runtimes.
The machines do not share a toolchain. A ratio to C absorbs how fast the hardware is. It does not absorb a different gcc, a different Go, or a different JVM, and each machine reports whatever it had. Where the runtime is itself the thing being measured — the Java and Kotlin rows — the machines align on a JDK major version, because otherwise a gap between two machines would confound the operating system with the virtual one. Everywhere else the version is declared rather than forced, and the toolchain table under each machine is that disclosure.
A ratio is a property of a machine until a second machine agrees. The first machine here failed to reproduce a ratio this project had published, by a factor of two, for exactly that reason. Until the table below has more than one column, read every number as measured rather than as true.
The programs are in bench/ports/ and the harness that times them is bench/ports/run.py. It builds what it finds a toolchain for, names what it skips, and prints an entry ready to append to the machine list. Every language uses 64-bit signed integers and, where it has the choice, the optimisation level keal build itself passes to cc.