The Rust compiler isn't slow; we are

Thank you for the update, it's true that the subject is hard to carry on and I totally understand the position :slight_smile:

I wanted to address this observation directly, as well. I use tokio+tonic in one of my projects. Starting with version 0.2, tokio gives you some levers to choose which parts you want to use with features. E.g., you probably don't need everything included in the full feature.

I was able to shave 30 seconds of build time off this project just by using fewer dependencies; I only use parts of futures-core and futures-util, so switching from the futures crate to just these two saved 23 seconds. I don't need hyper-tls so that's another optional dependency to remove, saving another 10 seconds. I can still go further with this project (now down to 2m 15s build time). Following the removal of those dependencies at least 30 seconds is spent in code generation within my crate, leaving about 1m 45s all to dependencies (104 in total).

Possibly related: An in-progress blog series (by @brson) on what slows the Rust compiler: Part 1, Part 2.

6 Likes

I am pretty new to Rust but from this documentation, it seems that Rust has incremental compilation turned on for non-release code. I assume when you use a library, you are not modifying it. So I am not sure why we have this long compile time issue.

You still have to build each dependency the first time you build the workspace. If you frequently make new workspaces for a project (e.g. a different workspace for each bug or feature you're working on), each workspace has to build all dependencies. This can make first-build time a relevant issue.

1 Like

The entire dependency graph also must be rebuilt every time the compiler is updated, which is about a 6-week cadence for stable users. Or every day for nightly users.

1 Like

It's only every day though if you actually update your compiler daily, which I would doubt is really necessary.

1 Like

I think you may have a small misunderstanding of what exactly is built from your dependencies.

Rust only compiles the used parts of the library - what I mean is, if you don't touch a type, it shouldn't end up in your binary.
The flip side of this is, even if you already depended on the library, when you touch a new type in that library, or instantiate a generic type in that library with a new type argument, then at least some portion of that library has to be newly compiled for the new portion of it you used.

Because of the way Rust generics work, there's not really a way around this (unlike, say C#, where JIT and the structure of the base Object type mean you shouldn't need to compile dependencies).

No, all non-monomorphic code is compiled the first time you compile the library. Using a new non-generic type will not require any recompilation of the library you're using.

Yes, any generic code is monomorphized at the use site. If you make a new instantiation of a generic parametrized type, that will compile all of its now monomorphizable code. And that will recompile anytime your crate is recompiled.

1 Like

Thanks for the clarification!

Yeah, I definitely don't know much about it. I just saw people discussing this and quickly checked the documentation and was confused. What you describe makes a lot of sense to me. I actually think it's quite clever to even just compile the part of the library that I use. I guess I never had a experience where you have to build it for 20+ minutes. For me, the worst situation (at work) is have to compile an old .NET project from scratch (due to some exotic way of hacking together the project) when touching certain parts of the codebase. Even that, is just about 3 minutes.

6 posts were split to a new topic: Git Branch vs Stash vs Tree

I don't personally have the time to analyze rust-analyzer's build times, but I've had a relatively good experience playing with measureme on a project of mine which had compile time issues, and would recommend anyone interested to try it out. There was a pretty good tutorial about it in a semi-recent Inside Rust blog post.

1 Like

There seems to be a typo at the start of the blog post:

I've been learning and using Rust for nearly four and a half years, since version 1.6.

Unless I am missing something, very likely, since I am new to Rust, the current stable Rust version is 1.44.1. The experience and the version don’t match up in the excerpt above.

Edit: very educational and wonderful post. Thank you. :blush:

Version 1.6 was released in January 2016: Announcing Rust 1.6 | Rust Blog

Could it be that you are confused about semantic versioning and suspect that 6 should be ordered after 44? These are just numbers in semver. Numerically 6 is sorted before 44.

There's a new 1.X release of Rust every six weeks with all of the incremental improvements since the last release. Rust publishes versions a lot faster than other languages, which tend to have slower feature release cadences. (Also keep in mind that there aren't separate compiler and language versions like there are for e.g. Java, which had at least 251 releases of JDK 8.)

Have you tried cargo-llvm-lines? I have had success with it recently. It's amazing how many instantiations of some generic functions you can end up with, dozens or even hundreds.

1 Like

Thank you both, @CAD97 and @parasyte. I appreciate the clarification.

With mixed success. It (and cargo-bloat as well) point to salsa being a big offender, but there's nothing immediately obviously optimizable: Salsa produces a lot of duplicate (?) monomorphisations · Issue #220 · salsa-rs/salsa · GitHub. We also hide majority of code calling salsa behind non-generic dyn functions, so, in theory, this shouldn't affect recompliations across the crate graph that much.

Am I correct that cargo llvm-lines shows only the output for the current crate? Is there some way to get crate graph wide info? With respect to monomorphisations, the property I care about most is that there are no big generic API (ie, I want all the code to be compiled in the defining crate, I want to avoid repeated monomorphisations in different downstream crates).

For the reference, here's output for cargo -Ztimings (for the final binary) and cargo llmv-lines (for the beefiest ra_ide crate): cargo llvm-lines · GitHub

EDIT: though, cargo llvm-lines did help to find one moderately easy win: Make `iterate_method_candidates` non-generic · Issue #4975 · rust-lang/rust-analyzer · GitHub

cargo llvm-lines does only show the output for the final crate, because it's built on top of cargo rustc. I don't know how to get counts for other crates other than checking them out separately and running cargo llvm-lines on them.

If you did those measurements with a stable compiler, there is some good news: soon (in 1.45, I think) the RawVec::grow and Iterator::try_fold and related entries will get smaller, thanks to #72013, #72166 and #72139.

3 Likes