Eight languages, one machine

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.

The four programs

They were chosen for four different costs. The most useful result on this page is that they do not agree on an ordering.

ProgramSizeWhat it stresses
fib(35)9.2M recursive callsCall overhead and integer arithmetic. Nothing is allocated, so a language's memory strategy cannot help or hurt it.
loops100M iterationsA while loop, a modulo and an addition. The body compiles to the same handful of instructions in every language that compiles.
objects10M allocationsA two-field record built and dropped inside every iteration. This is where reference counting, tracing collection and stack allocation give different answers.
lists1M elements through map / filter / foldEach 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.

Linux · aarch64 — 6-core aarch64 guest under QEMU, 7 GB

Ubuntu, Linux 7.0.02026-09-04keal 1.2.018 runs

Compute time

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.

Languagefib(35)loopsobjectslistsstartup
C9.143.110.13.90.2
C++9.343.810.25.80.3
Rust16.140.59.33.50.4
Keal20.444.210.013.40.3
Go22.746.211.130.90.8
Java15.343.914.297.314.9
Kotlin13.142.613.778.723.2
Python449.04 6841 46594.86.2

fib(35) — 9.2M recursive calls

C
1.0×
C++
1.0×
Rust
1.8×
Keal
2.2×
Go
2.5×
Java
1.7×
Kotlin
1.4×
Python
49×

loops — 100M iterations

C
1.0×
C++
1.0×
Rust
0.9×
Keal
1.0×
Go
1.1×
Java
1.0×
Kotlin
1.0×
Python
109×

objects — 10M allocations

C
1.0×
C++
1.0×
Rust
0.9×
Keal
1.0×
Go
1.1×
Java
1.4×
Kotlin
1.4×
Python
145×

lists — 1M elements through map / filter / fold

C
1.0×
C++
1.5×
Rust
0.9×
Keal
3.4×
Go
7.9×
Java
25×
Kotlin
20×
Python
24×

How solid each figure is

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.

Languagefib(35)loopsobjectslists
C9%7%8%19%
C++6%7%7%13%
Rust7%8%5%17%
Keal7%3%7%4%
Go3%5%5%89%
Java6%9%6%11%
Kotlin11%10%12%8%
Python8%3%5%7%

Toolchains

LanguageVersionBuild
Cgcc (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
Rustrustc 1.98.0 (88d9e12ae 2026-08-18)-C opt-level=2
Kealkeal 1.2.0keal build
Gogo version go1.25.1 linux/arm64go build
Javaopenjdk version "25.0.4" 2026-07-21javac, default JVM
Kotlinkotlinc-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
PythonCPython 3.14.4stock build

macOS · Apple M4 — 10-core Apple M4 (4 performance + 6 efficiency), arm64, bare metal, 24 GB — the only machine here that is not a guest

macOS 26.5.1, Darwin 25.5.02026-09-04keal 1.2.018 runs

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.

Compute time

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.

Languagefib(35)loopsobjectslistsstartup
C16.432.56.02.51.8
C++16.432.86.02.92.0
Rust16.233.49.12.91.9
Keal20.945.49.510.41.7
Go20.446.09.77.32.2
Java14.242.712.548.718.9
Kotlin17.041.813.041.526.3
Python605.35 8601 688106.615.6

fib(35) — 9.2M recursive calls

C
1.0×
C++
1.0×
Rust
1.0×
Keal
1.3×
Go
1.2×
Java
0.9×
Kotlin
1.0×
Python
37×

loops — 100M iterations

C
1.0×
C++
1.0×
Rust
1.0×
Keal
1.4×
Go
1.4×
Java
1.3×
Kotlin
1.3×
Python
180×

objects — 10M allocations

C
1.0×
C++
1.0×
Rust
1.5×
Keal
1.6×
Go
1.6×
Java
2.1×
Kotlin
2.2×
Python
281×

lists — 1M elements through map / filter / fold

C
1.0×
C++
1.2×
Rust
1.2×
Keal
4.2×
Go
2.9×
Java
19×
Kotlin
17×
Python
43×

How solid each figure is

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.

Languagefib(35)loopsobjectslists
C8%8%13%12%
C++7%11%5%7%
Rust7%11%6%16%
Keal9%2%5%10%
Go8%5%12%27%
Java21%8%8%10%
Kotlin10%14%7%12%
Python8%8%11%21%

Toolchains

LanguageVersionBuild
CApple 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
Rustrustc 1.98.0 (88d9e12ae 2026-08-18)-C opt-level=2
Kealkeal 1.2.0keal build
Gogo version go1.27.0 darwin/arm64go build
Javaopenjdk version "25.0.4.1" 2026-08-18javac, default JVM
Kotlinkotlinc-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
PythonCPython 3.14.7stock build

Windows · x86_64 — 4-core / 8-thread AMD EPYC 9354 guest on a Shadow cloud PC, 16 GB; a slice of a 32-core part, and the neighbours are outside our control

Windows 11, build 226212026-09-04keal 1.2.018 runs

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.

Compute time

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.

Languagefib(35)loopsobjectslistsstartup
C11.192.019.37.74.9
C++11.491.719.610.85.0
Rust21.666.720.36.65.9
Keal45.3108.218.417.66.2
Go41.483.519.623.66.9
Java26.5108.832.4101.746.9
Kotlin24.7108.033.185.663.4
Python1 1309 2782 896235.421.4

fib(35) — 9.2M recursive calls

C
1.0×
C++
1.0×
Rust
1.9×
Keal
4.1×
Go
3.7×
Java
2.4×
Kotlin
2.2×
Python
102×

loops — 100M iterations

C
1.0×
C++
1.0×
Rust
0.7×
Keal
1.2×
Go
0.9×
Java
1.2×
Kotlin
1.2×
Python
101×

objects — 10M allocations

C
1.0×
C++
1.0×
Rust
1.1×
Keal
1.0×
Go
1.0×
Java
1.7×
Kotlin
1.7×
Python
150×

lists — 1M elements through map / filter / fold

C
1.0×
C++
1.4×
Rust
0.9×
Keal
2.3×
Go
3.1×
Java
13×
Kotlin
11×
Python
31×

How solid each figure is

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.

Languagefib(35)loopsobjectslists
C6%1%3%9%
C++5%1%4%5%
Rust1%2%4%7%
Keal1%1%3%4%
Go2%2%4%19%
Java5%3%4%5%
Kotlin5%1%2%3%
Python1%3%4%2%

Toolchains

LanguageVersionBuild
Cgcc.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
Rustrustc 1.98.1 (48a229cea 2026-09-01)-C opt-level=2, x86_64-pc-windows-gnu
Kealkeal 1.2.0keal build (gcc, MinGW-w64 UCRT)
Gogo version go1.27.1 windows/amd64go build
Javaopenjdk version "25.0.4.1" 2026-08-18 LTSjavac, default JVM
Kotlinkotlinc-jvm 2.4.10 (JRE 25.0.4.1+1-LTS)-include-runtime, default JVM
PythonCPython 3.12.10stock build

The same ratios, machine by machine

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)

LanguageLinux · aarch64macOS · Apple M4Windows · x86_64
C1.0×1.0×1.0×
C++1.0×1.0×1.0×
Rust1.8×1.0×1.9×
Keal2.2×1.3×4.1×
Go2.5×1.2×3.7×
Java1.7×0.9×2.4×
Kotlin1.4×1.0×2.2×
Python49×37×102×

loops

LanguageLinux · aarch64macOS · Apple M4Windows · x86_64
C1.0×1.0×1.0×
C++1.0×1.0×1.0×
Rust0.9×1.0×0.7×
Keal1.0×1.4×1.2×
Go1.1×1.4×0.9×
Java1.0×1.3×1.2×
Kotlin1.0×1.3×1.2×
Python109×180×101×

objects

LanguageLinux · aarch64macOS · Apple M4Windows · x86_64
C1.0×1.0×1.0×
C++1.0×1.0×1.0×
Rust0.9×1.5×1.1×
Keal1.0×1.6×1.0×
Go1.1×1.6×1.0×
Java1.4×2.1×1.7×
Kotlin1.4×2.2×1.7×
Python145×281×150×

lists

LanguageLinux · aarch64macOS · Apple M4Windows · x86_64
C1.0×1.0×1.0×
C++1.5×1.2×1.4×
Rust0.9×1.2×0.9×
Keal3.4×4.2×2.3×
Go7.9×2.9×3.1×
Java25×19×13×
Kotlin20×17×11×
Python24×43×31×

What was checked before any of this was believed

A benchmark is an instrument, and an instrument nobody calibrated reports its own defects as findings. Each of these could have come back negative.

32 / 32 agree

The programs compute the same thing

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.

ruled out by scaling

No compiler computed the answer at build time

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.

two designs compared

Whether run order moves the numbers

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.

no drift

Nothing warmed up or wore out mid-run

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.

one discarded round

The page cache was full before the clock started

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.

named, not smoothed

Configurations too unstable to rank are said so

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.

What this does not establish

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.

Running it yourself

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.