Skip to content

Systems · 2025

Loop Speed Benchmark

One nested loop, 10,000 × 100,000 iterations, written in 20+ languages and timed. The numbers turned out to depend on more than the language, including how many times you run it.

1 billion loop iterations

20 implementations on an Apple M1 Pro, macOS 27.0.1. Bars are the median; each dot is one fresh process (2–10 runs per language). Log scale.

  • Go635 ms
  • C++636 ms
  • C640 ms
  • Rust641 ms
  • Kotlin698 ms
  • Java714 ms
  • Zig756 ms
  • Scala1.20 s
  • LuaJIT1.62 s
  • Swift2.03 s
  • JavaScript (Bun)2.06 s
  • C#2.07 s
  • JavaScript (Node)2.12 s
  • TypeScript (Node)2.13 s
  • PHP9.06 s
  • Lua16.41 s
  • Python31.66 s
  • Perl46.59 s
  • R90.19 s
  • Haskell (GHC -O2)139 s

Cold vs warm

Extra time on a cold launch: the full benchmark started right after clearing macOS’s file cache (sudo purge), compared with the warm launches that followed. The cost is roughly fixed per runtime, so it matters most for fast programs.

  • JavaScript (Node)+333 ms (+16%)
  • R+315 ms (<1%)
  • C#+266 ms (+13%)
  • PHP+266 ms (+3%)
  • Python+193 ms (+0.6%)
  • Scala+191 ms (+16%)
  • Java+186 ms (+26%)
  • Swift+179 ms (+9%)
  • Lua+177 ms (+1%)
  • Kotlin+162 ms (+23%)
  • JavaScript (Bun)+160 ms (+8%)
  • TypeScript (Node)+151 ms (+7%)
  • Rust+78 ms (+12%)
  • LuaJIT+77 ms (+5%)
  • C+++71 ms (+11%)
  • Go+60 ms (+9%)
  • Zig+36 ms (+5%)
  • C+33 ms (+5%)
  • Perl~0 ms (<1%)

Haskell is left out: its run-to-run spread (134–184 s) is far larger than any cold-start cost.

Run it in your browser

This runs live, on your device. Nothing above is replayed: press the button and your browser executes the benchmark loop right now.

cold
JavaScript:
the repo’s js/loop.js, compiled on the fly by your browser’s JIT.
WebAssembly:
the repo’s c/loop.c, compiled ahead of time into a 1.7 KB WebAssembly module.

Browsers can only run JavaScript and WebAssembly, so the other languages appear as recorded results. Your times are scaled to 1 billion iterations and added to the chart above as “· you”.

Iterations

Runs

Engines

12345678910Press Run benchmark: each run plots here, in order
run #JavaScriptWebAssembly (C)

Cold vs warm

The loop runs in a background worker that stays alive between clicks. On a cold worker, run 1 includes the JIT compiling the JavaScript; later runs, and “Run again”, reuse that compiled code. Because this benchmark is one hot loop, the JIT optimizes it partway through run 1, so the gap is small (a few milliseconds) and shrinks as the workload grows. WebAssembly is compiled before it runs, so it barely changes. Press Cold start to discard the compiled code and see it again. The “Cold vs warm” chart above shows the same thing for every language.

What I found

  • 01

    A cold start costs time, not a percentage

    After clearing the file cache, C's first launch took 33 ms longer, Java's 186 ms and Node's 333 ms: the bigger the runtime, the more there is to load. That's 5% of C's run and 27% of Java's, but under 1% of Python's 32 s.

  • 02

    JITs warm up inside a process

    Run 10 passes in one process and Java's first two passes take 10–13 ms; from pass 3 on it runs at 6.3 ms, the same as C. Node, Bun and C# also speed up after a pass or two, but settle around 20 ms.

  • 03

    Even C warms up

    When the CPU had been idle, or a program had just been built, C's first pass took up to 2× longer and reached full speed after about 70 ms. Run it again right away and it's flat from pass 1, so that's the CPU ramping up, not the language.

  • 04

    Measure the harness too

    My first pass launched every run through sh -c, which costs 0.285 s per launch on this Mac (/usr/bin/true took 0.29 s). That was 45% of C's runtime, so I re-ran everything with direct exec.

  • 05

    Small semantic differences cost real time

    C, C++, Rust and Go finish within 1% of each other. Zig's @mod is floored modulo rather than C's truncating %, and ran 18% slower. Swift's Int is 64-bit with overflow-checked addition, and took 3.2× as long as C.

  • 06

    Idiomatic isn't always fast

    The Haskell version writes to a boxed IOArray, which stores unevaluated thunks instead of numbers. Even at -O2 it grew to 1.3 GB and took 134–184 s depending on the run, over 200× C.

  • 07

    Audit before you chart

    5 of the 25 entries don't run the workload: three precompute the sum, one compiles C from Bash, and one Assembly version crashes on its first store. They're listed below the chart instead of on it.

Left off the chart

These versions skip the billion iterations, so their times aren’t comparable to the rest.

  • Assembly4 msCrashes on its first store: the remainder is written into W8, the register holding the array's address. It also hardcodes the input as 5 and stores 2 × (j mod 5) instead of adding to a[i]. The time is a crash, not a loop.
  • Go (optimized)6 msComputes the 100,000-step inner sum once per remainder (7 times) and fans the 10,000 writes out to goroutines with atomic adds: about 700K iterations instead of 1B.
  • Ruby3.25 sThe repo's Ruby version precomputes the inner sum 7 times instead of looping 10,000 × 100,000, so it does about 700K iterations. Nearly all of its time is interpreter startup through the rbenv shim (ruby -e 1 takes 3.1 s here).
  • Bash (optimized)1.12 sWrites a C program to /tmp, compiles it with gcc -O3 and runs it, so the time is gcc plus C, not Bash.
  • Perl JIT292 msImports B::JIT, which isn't a real CPAN module, so it exits with an error before doing any work. The original script timed that error.
All 20+ implementations on GitHub