Why Rust Finishes That Loop Before Python, PHP, JavaScript, and Go
Blog article/Blog archive

Why Rust Finishes That Loop Before Python, PHP, JavaScript, and Go

Interpreter, compiled+GC, compiled without GC on the path. Same loop. Slide the race to 100x.

Nov 2, 202612 min read0 comments1 view
RustSoftware Engineering

Do you want to receive these blog updates?

One email a week, only when there's a new post.

The clip is always the same shape. Five terminals, one loop, Rust prints the time first, and the caption says "100x". The clip never says what the loop did, or whether the program was waiting on a socket the whole time.

This post is a model for one kind of work: a CPU-bound checksum in memory. No HTTP. No disk. No Postgres. The interesting part is not the screenshot. It is which kind of language is running the loop.

Three lanes. Interpreted Python, PHP and JavaScript run through an interpreter with a dispatch tax on every loop trip. Go compiles ahead of time but keeps a garbage collector. Rust compiles to tight machine code with no GC on the path. Relative speed: about 1x, 3x and 13x, then 53x, then 115x
Same checksum loop, three ways to reach the CPU. Interpreters pay a dispatch tax every trip. Go compiles ahead of time but keeps a GC. Rust compiles to machine code with no GC on this path.
Animated demo of the interactive figure

This figure is interactive on the original post, where you can play with it live. Open the interactive demo on harun.dev.

At 1× the balls stay readable. Slide toward 100× when you want Rust to look unfair. The slider only changes playback. It does not change the language ratios.

Three kinds of language

Interpreted high-level: Python, PHP, JavaScript. You ship source. CPython, Zend, or V8 still walks bytecode (or JITs after warmup) while the loop runs. That dispatch cost is why these three sit at about 1×, 3×, and 13×.

Compiled high-level: Go. The compiler builds a native binary ahead of time, so the hot path is machine code. A garbage collector and runtime still ride along. That is why Go jumps to about 53× without matching Rust.

Compiled low-level: Rust. rustc and LLVM also emit machine code ahead of time. On this checksum there is no GC on the path: ownership did that work at compile time. That is the ~115× lane.

The program under test

Here is the job in Python. The other languages do the same math: walk 0 .. n-1, accumulate i * i, keep a 32-bit mask so the optimizer cannot delete the loop.

def hrr_checksum(n: int) -> int:
    total = 0
    for i in range(n):
        total = (total + i * i) & 0xFFFFFFFF
    return total

print(hrr_checksum(50_000_000))

Same shape in Rust:

fn hrr_checksum(n: u64) -> u32 {
    let mut total: u32 = 0;
    for i in 0..n {
        total = total.wrapping_add(((i * i) as u32));
    }
    total
}

fn main() {
    println!("{}", hrr_checksum(50_000_000));
}

I am not timing a framework, an ORM, or a JSON handler. If your real p99 is waiting on the network, this post is the wrong tool.

What the model is measuring

Python is the 1× baseline. The ratios below are relative throughput for that loop class on stock runtimes, rounded so the race stays readable.

Language Relative speed What usually eats the time
Python 1× Interpreter dispatch per iteration
PHP 3× Still managed, often a bit tighter than CPython here
JavaScript 13× JIT warms up and accelerates the hot path
Go 53× Compiled binary, GC still in the picture
Rust 115× Compiled binary, no GC on this path

PHP stays near the managed cluster (~3×). JavaScript’s JIT pulls it ahead of PHP (~13×) without catching a compiled binary. Go is clearly ahead (~53×). Rust is clearly ahead of Go (~115×, about 2× Go).

Why the interpreted lane trails

CPython and the PHP CLI walk bytecode (or similar) for every trip around the loop. Stock JavaScript is still a managed runtime, even when V8 helps after warmup. On ordinary CPU work they trail a compiled binary. That is why those three balls stay behind Go and Rust in the figure.

Why Go and Rust pull away, and why they split

Go and Rust both ship machine code for this loop, so they leave the interpreted lane behind. Go still carries a GC runtime. Rust does not on this path. That gap is the difference between ~53× and ~115× here: not "Go is slow," and not a web framework benchmark.

None of that says a Rust HTTP handler beats a Go one when both wait on the same database.

When the chart lies

Three cases where the figure should not decide the stack:

  • I/O bound work. Network, disk, and locks dominate. A faster loop language does not move the p99.
  • One-shot scripts. A migration you run once finishes in Python before I have a Rust project compiling.
  • Team fit. If nobody wants a borrow checker in review, Go or JavaScript will ship the service, and the micro-loop inside it is rarely the thing on fire.

I reach for Rust when the same CPU loop sits on the hot path, the data is in memory, and I can afford the compile. I do not reach for it to win a screenshot.

Hope you enjoyed this one. If you've got a loop that is actually hot, find me on X at https://x.com/harundotdev, and grab every other link from my bio at https://harun.dev/bio.

Get our services

Want this applied on your product? Browse all services.

Not sure which one fits? Book a consultation and we'll work out the right next step for your setup together.

On this post

Comments

A reply stays under the note it answers.

0 comments

No comments yet.

If you have a note on Why Rust Finishes That Loop Before Python, PHP, JavaScript, and Go, sign in and leave it.