I know these types of benchmarks are mostly just fun exhibition and should be taken with a huge grain of salt when comparing real-world performance of a language, but I am actually surprised that Rust managed to take second place, and C is sitting in 7th place. If I was forced to make a prediction without seeing the results, I would have ordered them something like this:
- Zig and C in the top two slots
- C++
- Rust
- Haskell, Nim, Odin in the next few slots
(should I be embarrassed for having never even heard of chapel?).
Without having watched the video (I find Fridman quite insufferable) I would take those results (or any âlanguage X produces faster code than language Yâ benchmarks) with a massive grain of salt, since the devil is usually in the details.
At least all compiled languages that sit on top of LLVM should have identical performance and in fact output identical code (with optimizations enabled and any âsafety overheadâ like range checks disabled), because all the performance optimization heavy lifting happens down in the LLVM optimizer passes - and as long as those optimizer passes have all the information they need theyâll produce the same output for the same input code, no matter what language the input code was written in.
If there are measurable differences I would rather go looking for differences in the build process (like Zig picking -march=native by default or Clang not automatically using LTO).
There is one theoretical advantage in Rust in that it guarantees that pointers canât alias, in C this requires careful use of the restrict keyword, but AFAIK in practice this hardly shows up in profiling outside artificial benchmarks.
The design/structure of a the language can often prohibit it, and make it impossible for LLVM to be able to make certain optimizations simply because it canât safely make certain assumptions. Andrewâs recent âDonât Forget to Flushâ presentation had a very good example illustrating this, and IIRC Rust was one the example languages used there for comparison. In theory it could be true, but in practice there can still be significant differences between two similar languages, both using LLVM, even for basic constructs.
IME those are more often stdlib design warts/philosophies which might make the work for the compiler harder or easier. But any compiled language should be able to drop down to simple expressions and memory load/stores.
For the record, I consider language and stdlib separate things, even though some languages (like C++ or Rust) blur the line.
(e.g. we should separate between benchmarks that measure language performance from benchmarks that measure performance of specific stdlib features, those can drastically differ of course)
I do generally agree, but there are minor differences that can have a significant impact, even between two analogous features. I wonât claim to be an expert on the internals of either Rust of Zig, but even constructs like unreachable differ in their meaning to the compiler. From what I understand, the unreachable macro in Rust is essentially the same as Zigâs keyword in Debug and ReleaseSafe modes, but in ReleaseFast, it can further take advantage to not even generate a panic and do undefined behavior instead, which allows even more aggressive optimizations to be made that are not possible in Rust. To the compiler, this semantic difference of âshould not be possible, but panic if it doesâ and âlogically impossible and no need to even handle itâ can result in different machine code.
How often this is actually impactful in the real-world is obviously open for debate, but even at the language level or trying to milk the CPU for every cycle you can get for a competitive benchmark, it can make a difference, even though both are using the same backend to generate the machine code.
For it to become UB you need unreachable_unchecked but iirc there are no buildin which works exactly as zigâs version with regards to optimization mode
EDIT: i was wrong it behaves exactly as zigâs version Rust Playground and panics in debug mode
Zig SHOWTIME talk on the Zig implementation at the time:
Rustâs equivalent is this: unreachable in std::intrinsics - Rust
âŚand in Clang it is __builtin_unreachable(), and I bet they all simply compile down to the LLVM unreachable instruction: LLVM Language Reference Manual â LLVM 22.0.0git documentation (in MSVC the same thing is called __assume(false)).
I have used this in one place so far in my home computer emulators: if you put an unreachable into a switch default prong, the compiler will remove an initial jump table range check. But this also means that if the default prong is actually called that the code will access a slot outside the jump table and jump to some random location.
I agree with you, deep down every languages that depends on LLVM can and will output the same machine code, given the same code/informations, but in practice we ought to remember that we are humans, and that some languages makes it easier to get performant code, whilst other at times can feel like a battle, and this is due to a lot of parameters, syntax, type system, compiler, tooling, stdlib, ecosystem etc. My point being that for example in Zig I always use a fitting allocator, I always try to think about memory, optimization, and good error handling, and thatâs because the language makes it very convenient to do all of those things at the same time. whereas in C for example, making your own allocator is more painful, and you are essentially on your own. In C++ you have to be very carefull, and not make even the tiniest mistake, and again like C it doesnât really play nice with the ecosystem. Even Rust in this case makes it trivial to write performant code because you can most often than not grab a high quality package or library for your needs. So in practice a language and the choices that were made to design it have IMO a tremendous impact on practical performance.
Rust is pretty big on no_std tho