8 ms·
Performance of Rust Language [pdf]
- encodedrose 4mo agoIf I followed, Rust's memory safety guarantee means sacrificing roughly ~3% performance with some worst case paths being ~15% (compared to C++ performance)?
- marcosdumay 4mo agoThat's on the typical performance for bounds checking in C too. But no, "memory safety" includes most of the things discussed on the slides, and those number are for bounds checking only.
- encodedrose 4mo agoAh, I was using GH's webui instead of downloading to view the PDF and it stopped loading at slide#47...rereading it now paints a much better picture. Thanks!
- gobdovan 4mo agoRust 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.
- afdbcreid 4mo agoThe overhead of bounds checking varies a lot. In the common case it's negligible (few percents), but in some cases, depending on what you build, it can go up to even 20%. And if it prevents autovectorization it can cost even more. There are techniques to minimize the perf loss, though (safely), and of course you can use unsafe code. If you do it smartly, in the vast majority of cases bound checks do not matter (in fact, even in C++ there is a push for a hardened standard library that does bound checks, and e.g. Google uses that). Rust will never include full proofs, but it might include ranged integers which can minimize bound checks even more.
- CrazyboyQCD 4mo ago[dead]
- aw1621107 4mo ago> 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 Not sure if I'm just out of the loop, but I'm having a hard time following this line of reasoning. Why is a public and/or stable contract needed to carry high-level semantics from HIR to MIR? Neither seems necessary to me; from what I understand HIR and MIR are rustc-internal so public contracts shouldn't matter, and the lack of stability means the Rust devs aren't precluded by backwards compatibility from modifying the IRs to add the ability to carry such invariants.
- gobdovan 4mo agoWhoops! Although there is no public contract between HIR and MIR, the public part was not relevant here. What I wanted to highlight is that if they'd want to add proper proof machinery to eliminate low-level safety checks, they'd have to do it at: surface language, which is already complex enough; then HIR->MIR boundary with clean provenance (which I think current MIR would collapse too aggressively) and which may require a much clearer contract; then, even if they do the full front- and mid- ends properly, if you leave it up to LLVM, it ends up in SCEV, which is language neutral and would not be a good fit to support the proof obligations that would be specific to Rust. I dug up a proposal from 2021 around bounds check hoisting in MIR, and from the discussion, details are pretty thorny [0]. It's narrower than general proofs but the frictions are very similar. The easiest example that shows HIR -> MIR difficulties is the part around `for i in 0..32 { a[i] = 1; }`. At the source level the range fact is super obvious, but after the for-loop/iterator lowering the MIR optimiser has to recover that `i` comes from exactly that range before it can turn 32 checks into the one hot-path check. Then it also would have to check for panic strategy to maintain the correct behaviour after optimisation. [0] https://github.com/rust-lang/rust/issues/92327 https://github.com/rust-lang/rust/issues/92327
- aw1621107 4mo agoOK, I think that makes more sense. Thanks for taking the time to explain!
- 4mo ago
- IshKebab 4mo agoThe 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.
- IcyWindows 4mo agoDo they even count towards safety if they aren't in release mode?
- IshKebab 4mo agoYou can enable them in release mode optionally. But I would say not. Really we need ISAs to provide a no-overhead way to check integer overflow.
- peterfirefly 4mo agoYou 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.
- Animats 4mo agoThere'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.
- afdbcreid 4mo agoCurrently LLVM cannot do that because the panic message includes the erroneous index. You can do it manually though if you add `_ = tab[100]`. Even if the panic message would not include the index, LLVM was unable to do that if the previous iterations had side effects (for example if `tab` is not a local variable).
- simonask 4mo agoPanics in Rust do not currently time-travel like that (including panics from failed bounds checks), and that's a good thing. The reason is that panicking does not imply terminating the process - they can be caught and handled, just like exceptions in C++. In fact, they use the same stack unwinding mechanism by default. What the compiler is allowed to do is to shorten the loop by one and unconditionally panic after the loop, but this falls under the purview of the LLVM optimizer.
- jojomodding 4mo agoOnce it shortens the loop, the compiler can also observe that `tab` is a local variable and therefore move the writes up "to the initializer." It can then see that the variable is unused and delete it, and also delete the loop.
- edevrk 4mo ago[flagged]
- 4mo ago
- jandrewrogers 4mo agoI 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.
- afdbcreid 4mo agoIs C++ more performant than C? I find this hard to believe. C++ does not have any construct that cannot be replicated, or is not common, in C. The only candidate is using virtualization and void* pointers instead of monomorphized generics which some C code does for the lack of better options, but that's not a problem in Rust as well. If anything, Rust has the potential to be more performant than C due to its aliasing rules (C has `restrict` but it's rarely used, standard C++ does not have even that). The current perf stats show it does make Rust code faster but just a little bit, although we don't utilize the full optimization potential currently (LLVM does not do many possible optimizations here, and `noalias` is weaker than Rust's aliasing rules). It can also affect autovectorization, and if it does the effect could be dramatic.
- loeg 4mo agoC++ you get templated generic algorithms that in practice no one really does with C because macros suck too much. So in C typically you'd have a runtime generic routine that doesn't inline. A classic example here is qsort() vs std::sort().
- afdbcreid 4mo agoI explicitly acknowledged that: > The only candidate is using virtualization and void* pointers instead of monomorphized generics which some C code does for the lack of better options, but that's not a problem in Rust as well. But in fact, if speed is a concern to you, even in C you will use "templated" sorting (via macros or code generation).
- _alphageek 4mo agoI 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.
- smasher164 4mo agoYou 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/ 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.
- suis_siva 4mo agoI 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++.
- DeathArrow 4mo agoI 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.
- Panzerschrek 4mo agoFor 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.
- ozgrakkurt 4mo agoThere 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
- jccx70 4mo ago[dead]
- jarym 4mo agoI'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
- wongarsu 4mo agoI'd assume mostly by avoiding the need for frequent rebuilds. Incremental builds are pretty fast (at least fast enough for my needs on a moderate codebase), full rebuilds can be brutal There are also some optimization tricks related to how you split your code among crates, since a unit of compilation is mostly one crate. Putting your FFI code in a separate crate (-sys crates are the norm) and splitting some of your code in libraries that can be compiled in parallel are the common examples
- unsolved73 4mo agothe linking of the project can take more time than actual compilation. Use the lld linker instead of the default one, see https://kerkour.com/rust-production-checklist#use-the-lld-linker https://kerkour.com/rust-production-checklist#use-the-lld-li...
- peterfirefly 4mo agoOn Windows? Use a dev drive.
- cauterize 4mo agokache helps
- culebron21 4mo agoI split into subcrates and also reduce the number of proc macros (e.g. got rid of Serde).
- sspoisk 4mo ago[flagged]
- yogthos 4mo agoI 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.
- DauntingPear7 4mo agoYeah rust has issues. Have you tried splitting your project into multiple crates if possible?
- yogthos 4mo agoI haven't tried that yet, I guess that would be an option once I get a few stable pieces that I'm not likely to be touching much.
- mswphd 4mo agofor context: in rust the "translation unit" is a crate (in C++, each source file is its own translation unit). So you only get parallelism across crates when compiling in rust. When you have a single-crate project, this means that 1. you get parallelism across all your dependencies, but 2. your (final) crate is serial. Splitting your crate can therefore get you parallelism back in step 2, which can be a (compilation) perf win. You can also get a caching benefit when there aren't changes, but even absent that you can get wins. It can come at the cost of runtime performance though, as rust won't (by default) do things like inline across translation units iirc. You can get back some of this perf back via various tricks (for example link-time optimization). Looks like the rust performance book has some writing on these tradeoffs. I haven't read these articles, but the table of contents on the left seems reasonable enough. https://nnethercote.github.io/perf-book/build-configuration.html https://nnethercote.github.io/perf-book/build-configuration.... Again I haven't read that (so I'm assuming it says the following), but you can get "best of both worlds" by configuring debug builds to not do LTO (= faster compilation speed, slower runtime), and release builds to do LTO (= faster runtime, slower compilation speed). There are also variants of LTO that make various tradeoffs you could look into. That article also links to the `cargo-wizard` subcommand of cargo https://github.com/Kobzol/cargo-wizard https://github.com/Kobzol/cargo-wizard it won't auto-split crates for you (of course), but it does seem to give you some default configurations of `cargo`, one of which configures for faster compilation speed. could be an easy way to mess around with things.
- sibidharan 4mo ago[dead]