- There's a stark jump of times going from ~200ms to ~900ms. (Rust v1.92.0 being an in-between outlier.)
- C# gets massive boost (990->225ms) when using SIMD.
- But C++ somehow gets slower when using SIMD.
- Zig very fast*!
- Rust got big boost (630ms->230ms) upgrading v1.92.0->1.94.0.
- Nim (that compiles to C then native via GCC) somehow faster than GCC-compiled C.
- Julia keeps proving high-level languages can be fast too**.
- Swift gets faster when using SIMD but loses much accuracy.
- Go fastest language with own compiler (ie not dependent to GCC/LLVM).
- V (also compiles to C) expected it (appearing similar) be close to Nim.
- Odin (LLVM) & Ada (GCC) surprisingly slow. (Was expecting them to be close to Zig/Fortran.)
- Crystal slowest LLVM-based language.
- Pure CPython unsurpassable turtle.
Curious how D's reference compiler (DMD) compares to the LLVM/GCC front-ends, how LFortran to gfortran, and QBE to GCC/LLVM. Also would like to see Scala Native (Scala currently being inside the 900~1000ms bunch).
* Note that uses `@setFloatMode(.Optimized)` which according to docs is equivalent to `--fast-math` but only D/Fortran use this flag (C/C++ do not).
** Uses `@fastmath` AND `@simd`. The comparison supposedly is for performance on idiomatic code and for Julia SIMD is a simple annotation applied to the loop (and Julia may even auto do it) but should still be noted because (as seen in C# example) it can be big.
Author here! Thanks for the detailed breakdown. Let me address a few points:
- C++ SIMD being slower: The standard C++ uses i & 0x1 which lets the compiler auto-vectorize. With -O3 -ffast-math -march=native, gcc/clang do this really well. The explicit AVX2 version has overhead from manual vector setup and horizontal sum at the end. Modern compilers often beat hand-written SIMD for simple loops like this.
- Zig fast-math: Correct. Line 5 has @setFloatMode(.optimized) with a comment saying "like C -ffast-math".
- Julia: Also correct. Uses @fastmath @simd for - both annotations together.
- Crystal/Odin/Ada being slow: All three use x = -x which creates a loop-carried dependency that blocks auto-vectorization. The fast implementations use the branchless i & 0x1 trick instead.
- C# SIMD: Uses Vector512 doing 8 doubles per iteration. That explains the ~4x speedup.
- Nim vs C: Both compile via gcc with similar flags. Probably just measurement variance.
- Fortran: Interestingly does NOT use -ffast-math. Uses manual loop unrolling instead (processes 4 terms per iteration).
- Go: You're right that it's the fastest with its own compiler. No LLVM/GCC backend, just Go's own SSA-based compiler.
For suggestions - DMD, LFortran, and Scala Native would be great additions. PRs welcome!
The code looks 100% identical except for the namespace prefixes. Must be something particular about github setup, because on mine (gcc15.2.1/clang20.1.8/Ryzen5600X) the run time is indistinguishably close. Interestingly, with default flags but -O3 clang is 30% slower, with flags from the script (-s -static -flto $MARCH_FLAG -mtune=native -fomit-frame-pointer -fno-signed-zeros -fno-trapping-math -fassociative-math) clang is a bit faster.
A nitpick is that benchmarking C/C++ with $MARCH_FLAG -mtune=native and math magic is kinda unfair for Zig/Julia (Nim seem to support those) - unless you are running Gentoo it's unlikely to be used for real applications.
If i'm understanding the repository correctly, it looks like each language reads from a file, does some I/O printing to console, then computes the value, then some more console printing and exits.
In my opinion, the comparisons could be better if the file I/O and console printing were removed.
Author here — yep, you understood the repository correctly.
My Python version is a good example of the structure: read rounds.txt, run the loop, print the result, exit. I’m timing the whole program with hyperfine.
I agree that for a “pure compute” microbenchmark you could remove file I/O and console output. I kept them mainly because:
- It gives every language the same simple interface (same input, same output) and acts as a basic correctness/sanity check.
- The benchmark runs 1 billion iterations. The file read and a single print happen once per run, so that overhead is tiny compared to the loop, and the results stay comparable in practice.
That said, I’m not against a compute-only / quiet mode. Since hyperfine already handles timing externally, the real work is implementing and maintaining a consistent --quiet / --no-io variant across 50+ languages.
If someone wants to contribute that (even starting with a subset), I’m happy to review PRs.
The Clojure version is not AOT'd, so it's measuring startup + compiler time. When properly compiled it should be comparable to the Java implementation.
I think I get why C++ thru C are all similar (all compile to similar assembly?), but I don't get why Go thru maybe Racket are all in what looks like a pretty narrow clump. Is there a common element there?
This is meaningless. The benchmarks are (1) run in github actions, (2) include file and console IO, and (3) are compiled with different compiler flags...
This sort of thing is pretty meaningless unless the code is all written by people who know how to get performance out of their languages (and they're allowed to do so). Did you use the right representation of the data? Did you use the right library? Did you use the right optimization options? Did you choose the fast compiler or the slow one? Did you know there was a faster or slower one? If you're using fancy stuff, did you use it right?
I did the same sort of thing with the Seive of Eratosthenes once, on a smaller scale. My Haskell and Python implementations varied by almost a factor of 4 (although you could argue that I changed the algorithm too much on the fastest Python one). OK, yes, all the Haskell ones were faster than the fastest Python one, and the C one was another 4 times faster than the fastest Haskell one... but they were still over the place.
Startup time doesn’t seem to be factored in correctly, so any language that uses a bytecode (e.g. Java) or is compiling from source (e.g. Ruby, Python, etc.) will look poor on this. If the kids of applications that you write are ones that exit after a fraction of a second, then sure, this will tell you something. But if you’re writing server apps that run for days/weeks/months, then this is useless.
Sure, but at the end of the day, youre still just polishing a turd, and you give up a LOT of ecosystem just to get a still deeply unimpressive benchmark time.
After seeing Swift's result I had to look into the source to confirm that yes, it was not written by someone who knows the language.
But this is a good benchmark results that demonstrate what performance level can you expect from every language when someone not versed in it does the code porting. Fair play
Author here. There are actually 3 Swift versions in the benchmark:
- Swift (standard): 893ms
- Swift (relaxed): 903ms (uses fast-math equivalent)
- Swift (SIMD): 509ms (explicit SIMD4)
The standard version uses x *= -1.0 which creates a loop-carried dependency that blocks auto-vectorization - same issue as Crystal, Odin, Ada. The SIMD version uses the branchless i & 0x1 trick and is ~1.75x faster.
Fair point that someone versed in Swift would probably use the better pattern in the standard version too. PRs welcome to improve it! The goal was idiomatic-ish code, but I'm not an expert in all 40+ languages.
Some implementations seem vectorization-friendly like the C one that uses a bit-twiddling trick to avoid the `x = -x` line that the Odin implementation and others have.
When you put these programs into Godbolt to see what's going on with them, so much of the code is just the I/O part that it's annoying to analyze
Reading the fine print, the benchmark is not just the Leibniz formula like it says in the chart title. It also includes file I/O, startup time, and console printing:
> Why do you also count reading a file and printing the output?
> Because I think this is a more realistic scenario to compare speeds.
Which is fine, but should be noted more prominently. The startup time and console printing obviously aren't relevant for something like the Python run, but at the top of the chart where runs are a fraction of a second it probably accounts for a lot of the differences.
Running the inner loop 100 times over would have made the other effects negligible. As written, trying to measure millisecond differences between entire programs isn't really useful unless someone has a highly specific use case where they're re-running a program for fractions of a second instead of using a long-running process.
That first big jump in the graph, I thought that it must be the divide between auto-gc'd and non auto-gc'd languages. But then I noticed that Rust is on the wrong side of the divide.
It would be nice to have a different color for languages that give the exact IEEE 754 result. Other languages can be using some kind of fast math. Sorted by accuracy:
# Language Accuracy
14 Swift (SIMD) 8.69
9 Fortran 90 9.44
2 C# (SIMD) 9.49
. [All the others] 9.50
I’m genuinely blown away by all the interest in what started as a silly little experiment.
The project grew way beyond its original scope.
My initial curiosity was simply: how could you set up a pipeline to do automatic speed comparisons? I was less interested in the results as a definitive measure and more in the infrastructure challenge itself.
But then the interest kept growing. I tried to modernize things, but one thing became quite notable: the difference between a language that gets actively optimized by its community (like Julia) versus one that just sits there unoptimized is striking.
Honestly, I got overwhelmed. Managing all those implementations, keeping versions up to date, reviewing contributions—it was a lot. I basically tapped out for about a year.
Now I’m back, and with AI assistance, maintaining this has become much more realistic—updating versions, helping optimize implementations, etc. That said, I’m always happy to accept contributions from folks who know their languages better than I do.
Thank you all for your interest and the thoughtful discussion!
33 comments
[ 3.0 ms ] story [ 54.9 ms ] thread- C++ unsurpassable king.
- There's a stark jump of times going from ~200ms to ~900ms. (Rust v1.92.0 being an in-between outlier.)
- C# gets massive boost (990->225ms) when using SIMD.
- But C++ somehow gets slower when using SIMD.
- Zig very fast*!
- Rust got big boost (630ms->230ms) upgrading v1.92.0->1.94.0.
- Nim (that compiles to C then native via GCC) somehow faster than GCC-compiled C.
- Julia keeps proving high-level languages can be fast too**.
- Swift gets faster when using SIMD but loses much accuracy.
- Go fastest language with own compiler (ie not dependent to GCC/LLVM).
- V (also compiles to C) expected it (appearing similar) be close to Nim.
- Odin (LLVM) & Ada (GCC) surprisingly slow. (Was expecting them to be close to Zig/Fortran.)
- Crystal slowest LLVM-based language.
- Pure CPython unsurpassable turtle.
Curious how D's reference compiler (DMD) compares to the LLVM/GCC front-ends, how LFortran to gfortran, and QBE to GCC/LLVM. Also would like to see Scala Native (Scala currently being inside the 900~1000ms bunch).
* Note that uses `@setFloatMode(.Optimized)` which according to docs is equivalent to `--fast-math` but only D/Fortran use this flag (C/C++ do not).
** Uses `@fastmath` AND `@simd`. The comparison supposedly is for performance on idiomatic code and for Julia SIMD is a simple annotation applied to the loop (and Julia may even auto do it) but should still be noted because (as seen in C# example) it can be big.
For the sub-second compiled languages, it's basically a benchmark of startup times, not performance in the hot loop.
The benchmark is definitely measuring the hot loop, not startup time.
- C++ SIMD being slower: The standard C++ uses i & 0x1 which lets the compiler auto-vectorize. With -O3 -ffast-math -march=native, gcc/clang do this really well. The explicit AVX2 version has overhead from manual vector setup and horizontal sum at the end. Modern compilers often beat hand-written SIMD for simple loops like this.
- Zig fast-math: Correct. Line 5 has @setFloatMode(.optimized) with a comment saying "like C -ffast-math".
- Julia: Also correct. Uses @fastmath @simd for - both annotations together.
- Crystal/Odin/Ada being slow: All three use x = -x which creates a loop-carried dependency that blocks auto-vectorization. The fast implementations use the branchless i & 0x1 trick instead.
- C# SIMD: Uses Vector512 doing 8 doubles per iteration. That explains the ~4x speedup.
- Nim vs C: Both compile via gcc with similar flags. Probably just measurement variance.
- Fortran: Interestingly does NOT use -ffast-math. Uses manual loop unrolling instead (processes 4 terms per iteration).
- Go: You're right that it's the fastest with its own compiler. No LLVM/GCC backend, just Go's own SSA-based compiler.
For suggestions - DMD, LFortran, and Scala Native would be great additions. PRs welcome!
On my machine (an old i7-8700), dmd performs rather poorly. 3.5 seconds.
Comparatively ldc runs in 943 milliseconds: I'm sure there is compiler switch magic that I don't know about that could improve them.A nitpick is that benchmarking C/C++ with $MARCH_FLAG -mtune=native and math magic is kinda unfair for Zig/Julia (Nim seem to support those) - unless you are running Gentoo it's unlikely to be used for real applications.
In my opinion, the comparisons could be better if the file I/O and console printing were removed.
My Python version is a good example of the structure: read rounds.txt, run the loop, print the result, exit. I’m timing the whole program with hyperfine.
I agree that for a “pure compute” microbenchmark you could remove file I/O and console output. I kept them mainly because:
- It gives every language the same simple interface (same input, same output) and acts as a basic correctness/sanity check.
- The benchmark runs 1 billion iterations. The file read and a single print happen once per run, so that overhead is tiny compared to the loop, and the results stay comparable in practice.
That said, I’m not against a compute-only / quiet mode. Since hyperfine already handles timing externally, the real work is implementing and maintaining a consistent --quiet / --no-io variant across 50+ languages.
If someone wants to contribute that (even starting with a subset), I’m happy to review PRs.
I did the same sort of thing with the Seive of Eratosthenes once, on a smaller scale. My Haskell and Python implementations varied by almost a factor of 4 (although you could argue that I changed the algorithm too much on the fastest Python one). OK, yes, all the Haskell ones were faster than the fastest Python one, and the C one was another 4 times faster than the fastest Haskell one... but they were still over the place.
But this is a good benchmark results that demonstrate what performance level can you expect from every language when someone not versed in it does the code porting. Fair play
Fair point that someone versed in Swift would probably use the better pattern in the standard version too. PRs welcome to improve it! The goal was idiomatic-ish code, but I'm not an expert in all 40+ languages.
When you put these programs into Godbolt to see what's going on with them, so much of the code is just the I/O part that it's annoying to analyze
> Why do you also count reading a file and printing the output?
> Because I think this is a more realistic scenario to compare speeds.
Which is fine, but should be noted more prominently. The startup time and console printing obviously aren't relevant for something like the Python run, but at the top of the chart where runs are a fraction of a second it probably accounts for a lot of the differences.
Running the inner loop 100 times over would have made the other effects negligible. As written, trying to measure millisecond differences between entire programs isn't really useful unless someone has a highly specific use case where they're re-running a program for fractions of a second instead of using a long-running process.
Swift: 3.7
Python: that's incorrect!
Swift: yeah, but it's fast!
There is very little superfluous or that cannot be inferred by the compiler here: https://github.com/niklas-heer/speed-comparison/blob/master/...
I’m genuinely blown away by all the interest in what started as a silly little experiment. The project grew way beyond its original scope. My initial curiosity was simply: how could you set up a pipeline to do automatic speed comparisons? I was less interested in the results as a definitive measure and more in the infrastructure challenge itself.
But then the interest kept growing. I tried to modernize things, but one thing became quite notable: the difference between a language that gets actively optimized by its community (like Julia) versus one that just sits there unoptimized is striking.
Honestly, I got overwhelmed. Managing all those implementations, keeping versions up to date, reviewing contributions—it was a lot. I basically tapped out for about a year.
Now I’m back, and with AI assistance, maintaining this has become much more realistic—updating versions, helping optimize implementations, etc. That said, I’m always happy to accept contributions from folks who know their languages better than I do.
Thank you all for your interest and the thoughtful discussion!