29 comments

[ 3.1 ms ] story [ 65.2 ms ] thread
If I followed, Rust's memory safety guarantee means sacrificing roughly ~3% performance with some worst case paths being ~15% (compared to C++ performance)?
Rust is in an awkward position of being already complicated enough that adding proofs for skipping bounds checks probably will not happen for a long time, even though this kind of low-level operation is where a lot of optimisation is lost.

Compounding on this, Rust is also unstable underneath, since there is no public, stable contract for carrying high-level semantics from HIR into MIR. Because these high-level invariants are lost during compilation, the compiler cannot easily use them to prove and eliminate low-level safety checks. But even if the frontend was perfect, Rust relies on LLVM's language-neutral SCEV, which operates purely on low-level math and cannot reason about high-level language semantics.

Ultimately, a lot of things would need to change for Rust to pay no performance for safety features.

The benchmarks in this talk show that the bounds checks are mostly insignificant, and actually it's the integer overflow checks that are far more costly.

Actually nm, I forgot those are disabled in release mode. Good decision I guess.

You can sometimes just add asserts for the index variable(s) and have the LLVM optimizer go "hmm, I should try to prove that those are true" and then get the range checks optimized away.
There's a discussion of "delayed bounds checking", but not "hoisted bounds checking", where bounds checking is done early. Consider

    let mut tab: [usize;100] = [0;100];
    ...
    for i in 0..101 {
        tab[i] = i;
    }
This must panic at i=100. Panic becomes inevitable at entry to the loop. Is the compiler entitled to generate a check that will panic at loop entry? The slides suggest that Rust does not hoist such checks, and, so, with nested loops, it has trouble getting checks out of the loop, which prevents vectorization.
I would summarize it thusly: Rust is roughly as performant as C. This matches my experience and Rust is more ergonomic than C in many regards. The caveat is that modern C++ is notably more performant than C and by implication Rust. This also matches my experience for both C and Rust.

I think most of this is attributable to the ergonomics of compile-time expressiveness. C++ can effortlessly do things that require mountains of ugly boilerplate and macros in C or Rust. In principle they can express the same things but no one wants to write or deal with that ugly boilerplate so the equivalency is never realized in real code bases.

Zig is interesting because it slots in as a C-like language with a competent and expressive compile-time story. I don’t use Zig but I recognize its game.

> modern C++ is notably more performant than C and by implication Rust

I don't think this holds. Rust has the same facilities which C++ has. Rust's metaprogramming capabilities are now on par with C++ (they weren't always). Rust has a similar generics implementation which allows it to do what C++ does in terms of method dispatch and generation. And now Rust has pretty much the same compile time constant generation capabilities that C++ has.

I don't think there's a part of C++ which isn't in Rust at this point. The only thing potentially missing is the experience and investment using those features.

Reflection, Rust community did a very good job driving away the person that cared to do that work, to the point he went back to C and C++ ISO comittees.

Several features on C23 were done thanks to his work.

Also compile time execution is much more rich in C++ than Rust, regardling language features and standard library that can be used at compile time.

Naturally none of the languages is standing still, and they will both improve on that regard.

>> The caveat is that modern C++ is notably more performant than C and by implication Rust.

Please provide proof for this outrageous statement.

C++ being more performant than C is not something that I've seen in any benchmarks or personal experience.

In practice, some of the cases about specialization that was made possible with C++ constructs is also achieved by modern C compilers.

I would have liked to have the checks-off delta plotted across rustc versions - the deck notes this stuff moves non-monotonically, so a trend line would say more than a single-version snapshot.
You end up needing something like refinement types to control the way you statically enforce bounds. That being said, there's stuff like https://flux-rs.github.io/flux/ which uses macros to layer a refinement type system on top of rust's. You can use it to statically eliminate bounds checks.
I worked professionally with C, C++, Zig and Rust (in that order). My experience is that writing performant code is by far the easiest in C++, and by far the most difficult in C. Most of this, in practical experience, is due to ergonomics, in my opinion.

Templates in C++ benefit from being part of the core language, -- stick a `template` above your `class`, and you're in metaprogramming land. Stick a template specialization, and you've done a niche optimization. You didn't need a separate crate or a whole macro DSL. Variadic templates are also really really nice for monomorphizing N-ary generic functions. The duck typing of templates makes

This is precisely where I struggle with Rust the most -- monomorphization is limited within generics, so you end up going to the `proc_macro` hell, which involves a separate crate, a separate Cargo.toml, etc.

Zig seems like it would fit the bill -- and doing micro-optimizations within zig is surprisingly easy. The language's comptime facilities allow for really good niche optimizations -- however, the language also has some strange decisions. The allocator interface is notoriously a vtable, so a lot of the DOD optimizations that andrewrk has spoken numerously of (and to be clear -- I did learn a lot about DOD from his talks back when I was a wee engineer), raise one of my eyebrows.

C seems like it should be fast, but implementing any data structure, any generic algorithm in C is impossible. Either you're copy-pasting, or you're making macro DSLs. None of which is great.

---

To further talk about the C++ situation -- the monomorphic allocator interface was always awesome. Compared to Zig's vtables and Rust's nothing (up until a couple days ago), having a way to pass custom allocators with types was awesome. The new std::pmr::* interfaces and containers are also really exciting -- monomorphization, as beautiful as it is, does cost a lot -- refactoring it is not easy, compilation times are a mess. Sometimes the right tool is a vtable interface, and, C++ gives you those facilities.

And this is C++'s no1 problem when it comes to performance too -- it's a leviathan -- it'll give you the tools to write REALLY fast code, but it will also give you inheritance -- forget about your caches then.

When I was working at Tesla, there were some pretty gnarly vtable jumps in firmware (of all places), and I suspect part of that could've been alleviated if people knew more about CRTP.

So, here's where I land -- C++ really will give you the tools it can to let you write the fastest code possible. But it will also give you the tools to make your code really slow. Committee language means everyone in the committee needs to be happy.

Rust, on the other hand, is really designed to promote safe-but-very-fast practices -- had the firmware that I discussed used Rust, my guess is that we would've gravitated towards generics and monomorphization, rather than the heavy dynamic inheritance. C++, when it comes to performance, as it does to all other things, is a barreled shotgun. Rust's design almost always promotes the best available pattern and that's why I rarely reach out for C++.

I was looking at Zig. It's performant, it's easier to reason about Zig code than Rust code but its api is unstable, there are a lot of breaking changes. Coding agents have a difficult time write proper Zig because of the breaking changes and of the small amount of new Zig code in the wild.
For a couple of years I have written an advanced software rasterizer (like in old PC games) using Rust. With a little bit of unsafe code it was doable and result performance was great. I only used unsafe in places mentioned in the article above, like in tight loops where the compiler's optimizer struggles to remove bounds checks and in a couple of places where CPU intrinsics were used.
There is no performance of language, it is very dependent on the compiler in any language. I don’t think even clang/gcc can “fully” optimize c
I've been doing more and more Rust. Even with sscache the compile times are not great so for any moderately sized codebase that requires frequent rebuilds I don't know how everyone else is doing it
I split into subcrates and also reduce the number of proc macros (e.g. got rid of Serde).
I find the real issue with Rust is the compiler performance. I decided to use it for a project, and frankly I have huge regrets now that it's grown big enough. It literally takes over a minute to compile on my M1 laptop.

I just don't understand how people find this sort of thing normal. If you implement a feature, and then you want to see it in action, the feedback loop for that is insanely slow. It's incredibly jarring coming from Clojure where you have a live development experience.

I've used Go before, and while you still have to wait for compiling, at least the compiler is actually fast.

I get the problem Rust aims to solve, but the ergonomics are just not there in my opinion.

See languages like D, Delphi, Ada, C++ (with modules/libraries, REPLs like CINT), Haskell (GHCi), OCaml (REPL) for similar complexity levels, and faster compile times.

It is a matter of tooling, eventually they will get there I guess.

I had similar problems. Compilation got 10x faster once I split one crate into workspace with crates inside. The checks are only done on the compiled one. Then, I got rid of serde, because its derive macros are heavy and add some seconds of hickup (other serialization frameworks based on proc macros are slow too: rkyv, musli).