Why is the Rust compiler so slow?
So there's this guy you may have heard of called Ryan Fleury who makes the RAD debugger for Epic. The whole thing is made with 278k lines of C and is built as a unity build (all the code is included into one file that is compiled as a single translation unit). On a decent windows machine it takes 1.5 seconds to do a clean compile. This seems like a clear case-study that compilation can be incredibly fast and makes me wonder why other languages like Rust and Swift can't just do something similar to achieve similar speeds.
I’m glad that Go went the other way around: compilation speed over optimization.
For the kind of work I do — writing servers, networking, and glue code — fast compilation is absolutely paramount. At the same time, I want some type safety, but not the overly obnoxious kind that won’t let me sloppily prototype. Also, the GC helps. So I’ll gladly pay the price. Not having to deal with sigil soup is another plus point.
I guess Google’s years of experience led to the conclusion that, for software development to scale, a simple type system, GC, and wicked fast compilation speed are more important than raw runtime throughput and semantic correctness. Given the amount of networking and large - scale infrastructure software written in Go, I think they absolutely nailed it.
But of course there are places where GC can’t be tolerated or correctness matters more than development speed. But I don’t work in that arena and am quite happy with the tradeoffs that Go made.
I suspect that it is because the borrow checker needs to do data flow analysis. DFA is normally done in the optimizer, and we all know that optimized builds are quite a bit slower, and that is due to DFA.
DFA in the front end means slow.
That person seems to be confused. Installing a single, statically linked binary is clearly simpler than managing a container?!
I don't really consider it to be slow at all. It seems about as performant as any other language this complexity, and it's far faster than the 15 minute C++ and Scala build times I'd place in the same category.
As a former C++ developer, claims that rust compilation is slow leave me scratching my head.
Incremental compilation good. If you want, freeze the initial incremental cache after a single fresh build to use for building/deploying updates, to mitigate the risk of intermediate states gradually corrupting the cache.
Works great with docker: upon new compiler version or major website update, rebuild the layer with the incremental cache; otherwise just run from the snapshot and build newest website update version/state, and upload/deploy the resulting static binary. Just set so that mere code changes won't force rebuilding the layer that caches/materializes the fresh clean build's incremental compilation cache.
My homepage takes 73ms to rebuild: 17ms to recompile the static site generator, then 56ms to run it.
andy@bark ~/d/andrewkelley.me (master)> zig build --watch -fincremental
Build Summary: 3/3 steps succeeded
install success
└─ run exe compile success 57ms MaxRSS:3M
└─ compile exe compile Debug native success 331ms
Build Summary: 3/3 steps succeeded
install success
└─ run exe compile success 56ms MaxRSS:3M
└─ compile exe compile Debug native success 17ms
watching 75 directories, 1 processes
> Vim hangs when you open it
you can enable word wrapping as a workaround ( `:set wrap`). Lifehack: it can be hard to navigate in such file with just `h, j, k, l`, but you can use `gh, gj, etc`. With `g` vim will work with visual lines, while without it with just lines splitted with LF/CRLF
Where is Cranelift mentioned
My 2c on this is nearly ditching rust for game development due to the compile times, in digging it turned out that LLVM is very slow regardless of opt level. Indeed it's what the Jai devs have been saying.
So Cranelift might be relevant for OP, I will shill it endlessly, took my game from 16 seconds to 4 seconds. Incredible work Cranelift team.
A lot of people are replying to the title instead of the article.
> To get your Rust program in a container, the typical approach you might find would be something like:
If you have `cargo build --target x86_64-unknown-linux-musl` in your build process you do not need to do this anywhere in your Dockerfile. You should compile and copy into /sbin or something.
If you really want to build in a docker image I would suggest using `cargo --target-dir=/target ...` and then run with `docker run --mount type-bind,...` and then copy out of the bind mount into /bin or wherever.
Rust compiler is very very fast but language has too many features.
The slowness is because everyone has to write code with generics and macros in Java Enterprise style in order to show they are smart with rust.
This is really sad to see but most libraries abuse codegen features really hard.
You have to write a lot of things manually if you want fast compilation in rust.
Compilation speed of code just doesn’t seem to be a priority in general with the community.
First time someone I know in real life has made it to the HN front page (hey sharnoff, congrats) anyway -
I think this post (accidentally?) conflates two different sources of slowness:
1) Building in docker 2) The compiler being "slow"
They mention they could use bind mounts, yet wanting a clean build environment - personally, I think that may be misguided. Rust with incremental builds is actually pretty fast and the time you lose fighting dockers caching would likely be made up in build times - since you'd generally build and deploy way more often than you'd fight the cache (which, you'd delete the cache and build from scratch in that case anyway)
So - for developers who build rust containers, I highly recommend either using cache mounts or building outside the container and adding just the binary to the image.
2) The compiler being slow - having experienced ocaml, go and scala for comparisons the rust compiler is slower than go and ocaml, sure, but for non interactive (ie, REPL like) workflows, this tends not to matter in my experience - realistically, using incremental builds in dev mode takes seconds, then once the code is working, you push to CI at which point you can often accept the (worst case?) scenario that it takes 20 minutes to build your container since you're free to go do other things.
So while I appreciate the deep research and great explanations, I don't think the rust compiler is actually slow, just slower than what people might be use to coming from typescript or go for example.
Meanwhile, other languages have a JIT compiler which compiles code as it runs. This would be great for development even if it turns out to be slower overall.
Related from a couple of weeks ago:
https://news.ycombinator.com/item?id=44234080
(Rust compiler performance; 287 points, 261 comments)
I've got to say when I come across an open source project and realise it's in rust I flinch a bit know how incredibly slow the build process is. It's certainly been one of the deterrents to learning it.
The Rust compiler is slow. But if you want more features from your compiler you need to have a slower compiler, there isn't a way around that. However this blog post doesn't really seem to be around that and more an annoyance in how they deploy binaries.
OP could have skipped all this by doing the compilation with cache on the host system and copying the compiled statically linked binary back to the docker image build.
you had a functional and minimal deployment process (compile copy restart) and now you have...
Just set up a build server and have your docker containers fetch prebuilt binaries from that?
> This is... not ideal.
What? That's absolutely ideal! It's incredibly simple. I wish deployment processes were always that simple! Docker is not going to make your deployment process simpler than that.
I did enjoy the deep dive into figuring out what was taking a long time when compiling.
This is such a weird cannon on sparrows approach.
The local builds are fast, why would you rebuild docker for small changes?
Also why is a personal page so much rust and so many dependencies. For a larger project with more complex stuff you’d have a test suite that takes time too. Run both in parallel in your CI and call it a day.
Unfortunately, removing debug symbols in most cases isn't a good/useful option
For deploying Rust servers, I use Spin WASM functions[1], so no Docker / Kubernetes is necessary. Not affiliated with them, just saying. I just build the final WASM binary and then the rest is managed by the runtime.
Sadly, the compile time is just as bad, but I think in this case the allocator is the biggest culprit, since disabling optimization will degrade run-time performance. The Rust team should maybe look into shipping their own bundled allocator, "native" allocators are highly unpredictable.
[^1]: https://www.fermyon.com
rust prioritises build-time correctness: no runtime linker or no dynamic deps. all checks (types, traits, ownership) happen before execution. this makes builds sensitive to upstream changes. docker uses content-hash layers, so small context edits invalidate caches. without careful layer ordering, rust gets fully recompiled on every change.
>Every time I wanted to make a change, I would:
>Build a new statically linked binary (with --target=x86_64-unknown-linux-musl) >Copy it to my server >Restart the website
Isn't it a basic C compiler feature that you can compile a file as an Object, and then link the objects into a single executable? Then you only recompile the file you changed.
Not sure what I'm missing.
why rust compiler create so BIG executable!
WRT compilation efficiency, the C/C++ model of compiling separate translation units in parallel seems like an advance over the Rust model (but obviously forecloses opportunities for whole-program optimization).
Is there an equivalent of ninja for rust yet?
Early design decisions favored run-time over compile-time [1]:
> * Borrowing — Rust’s defining feature. Its sophisticated pointer analysis spends compile-time to make run-time safe.
> * Monomorphization — Rust translates each generic instantiation into its own machine code, creating code bloat and increasing compile time.
> * Stack unwinding — stack unwinding after unrecoverable exceptions traverses the callstack backwards and runs cleanup code. It requires lots of compile-time book-keeping and code generation.
> * Build scripts — build scripts allow arbitrary code to be run at compile-time, and pull in their own dependencies that need to be compiled. Their unknown side-effects and unknown inputs and outputs limit assumptions tools can make about them, which e.g. limits caching opportunities.
> * Macros — macros require multiple passes to expand, expand to often surprising amounts of hidden code, and impose limitations on partial parsing. Procedural macros have negative impacts similar to build scripts.
> * LLVM backend — LLVM produces good machine code, but runs relatively slowly. Relying too much on the LLVM optimizer — Rust is well-known for generating a large quantity of LLVM IR and letting LLVM optimize it away. This is exacerbated by duplication from monomorphization.
> * Split compiler/package manager — although it is normal for languages to have a package manager separate from the compiler, in Rust at least this results in both cargo and rustc having imperfect and redundant information about the overall compilation pipeline. As more parts of the pipeline are short-circuited for efficiency, more metadata needs to be transferred between instances of the compiler, mostly through the filesystem, which has overhead.
> * Per-compilation-unit code-generation — rustc generates machine code each time it compiles a crate, but it doesn’t need to — with most Rust projects being statically linked, the machine code isn’t needed until the final link step. There may be efficiencies to be achieved by completely separating analysis and code generation.
> * Single-threaded compiler — ideally, all CPUs are occupied for the entire compilation. This is not close to true with Rust today. And with the original compiler being single-threaded, the language is not as friendly to parallel compilation as it might be. There are efforts going into parallelizing the compiler, but it may never use all your cores.
> * Trait coherence — Rust’s traits have a property called “coherence”, which makes it impossible to define implementations that conflict with each other. Trait coherence imposes restrictions on where code is allowed to live. As such, it is difficult to decompose Rust abstractions into, small, easily-parallelizable compilation units.
> * Tests next to code — Rust encourages tests to reside in the same codebase as the code they are testing. With Rust’s compilation model, this requires compiling and linking that code twice, which is expensive, particularly for large crates.
[1]: https://www.pingcap.com/blog/rust-compilation-model-calamity...
Some code that can make Rust compilation pathologically slow is complex const expressions. Because the compiler can evaluate a subset of expressions at compile time[1], a complex expression can take an unbounded amount of time to evaluate. The long-running-const-eval will by default abort the compilation if the evaluation takes too long.
Slow compile times are a feature, get to make a cuppa.
I don't think rustc is that slow. It's usually cargo/the dozens of crates that make it take a long time, even if you've set up a cache and rustc is doing nothing but hitting the cache.
It's not. It's just doing way more work than many other compilers, due to a sane type system.
Personally I don't care anymore, since I do hotpatching:
https://lib.rs/crates/subsecond
Zig is faster, but then again, Zig isn't memory save, so personally I don't care. It's an impressive language, I love the syntax, the simplicity. But I don't trust myself to keep all the memory relevant invariants in my head anymore as I used to do many years ago. So Zig isn't for me. Simply not the target audience.
Why doesn't the Rust ecosystem optimize around compile time? It seems a lot of these frameworks and libraries encourage doing things which are slow to compile.
TL;DR `async` considered harmful.
For all the C++ laughing in this thread, there's really only one thing that makes C++ slow - non-`extern` templates - and C++ gives you a lot more space to speed them up than Rust does.
tl;dr: it’s slow because it finds far more bugs before runtime than literally any other mainstream compiled language
tldr as always, don't use Musl, if you want performance, compatibility.
[flagged]
One aspect that I find interesting is that Rust projects often seem deceivingly small.
First, dependencies don't translate easily to the perceived size. In C++ dependencies on large projects are often vendored (or even not used at all). And so it is easy to look at your ~400000 line codebase and go "it's slow, but there's a lot of code here after all".
Second (and a much worse problem) are macros. They actually hit the same issue. A macro that expands to 10s or 100s of lines can very quickly take your 10000 line project and turn it into a million line behemoth.
Third are generics. They also suffer the same problem. Every generic instantiation is eating your CPU.
But I do want to offer a bit of an excuse for rust. Because these are great features. They turn what would have taken 100000 lines of C or 25000 lines of C++ to a few 1000s lines of Rust.
However, there is definitely an overuse here making the ecosystem seem slow. For instance, at work we use async-graphql. The library itself is great, but it's an absolute proc-macro hog. There's issues in the repository open for years about the performance. You can literally feel the compiler getting slower for each type you add.