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.
The more your compiler does for you at build time, the longer it will take to build, it's that simple.
Go has sub-second build times even on massive code-bases. Why? because it doesn't do a lot at build time. It has a simple module system, (relatively) simple type system, and leaves a whole bunch of stuff be handled by the GC at runtime. It's great for its intended use case.
When you have things like macros, advanced type systems, and want robustness guarantees at build time.. then you have to pay for that.
I think this is mostly a myth. If you look at Rust compiler benchmarks, while typechecking isn't _free_ it's also not the bottleneck.
A big reason that amalgamation builds of C and C++ can absolutely fly is because they aren't reparsing headers and generating exactly one object file so the linker has no work to do.
Once you add static linking to the toolchain (in all of its forms) things get really fucking slow.
Codegen is also a problem. Rust tends to generate a lot more code than C or C++, so while the compiler is done doing most of its typechecking work, the backend and assembler has a lot of things to chuck through.
That the type system is responsible for rust's slow builds is a common and enduring myth. `cargo check` (which just does typechecking) is actually usually pretty fast. Most of the build time is spent in the code generation phase. Some macros do cause problems as you mention, since the code that contains the macro must be compiled before the code that uses it, so they reduce parallelism.
Yes but I'd also add that Go specifically does not optimize well.
The compiler is optimized for compilation speed, not runtime performance. Generally speaking, it does well enough. Especially because it's usecase is often applications where "good enough" is good enough (IE, IO heavy applications).
You can see that with "gccgo". Slower to compile, faster to run.
Not really. The root reason behind Go's fast compilation is that it was specifically designed to compile fast. The implementation details are just a natural consequence of that design decision.
Since fast compilation was a goal, every part of the design was looked at through a rough "can this be a horrible bottleneck?", and discarded if so. For example, the import (package) system was designed to avoid the horrible, inefficient mess of C++. It's obvious that you never want to compile the same package more than once and that you need to support parallel package compilation. These may be blindingly obvious, but if you don't think about compilation speed at design time, you'll get this wrong and will never be able to fix it.
As far as optimizations vs compile speed goes, it's just a simple case of diminishing returns. Since Rust has maximum possible perfomance as a goal, it's forced to go well into the diminishing returns territory, sacrificing a ton of compile speed for minor performance improvements. Go has far more modest performance goals, so it can get 80% of the possible performance for only 20% of the compile cost. Rust can't afford to relax its stance because it's competing with languages like C++, and to some extent C, that are willing to go to any length to squeeze out an extra 1% of perfomance.
Dlang compilers does more than any C++ compiler (metaprogramming, a better template system and compile time execution) and it's hugely faster. Language syntax design has a role here.
This is sometimes called amalgamation and you can do it Rust as well. Either manually or with tools. The point is that apart from very specific niches it is just not a practical approach.
It's not that it can't be done but that it usually is not worth the hassle and our goal should be for compilation to be fast despite not everything being in one file.
Turbo Pascal is a prime example for a compiler that won the market not least because of its - for the time - outstanding compilation speed.
In the same vein, a language can be designed for fast compilation. Pascal in general was designed for single-pass compilation which made it naturally fast. All the necessary forward declarations were a pain though and the victory of languages that are not designed for single-pass compilation proofs that while doable it was not worth it in the end.
Because Russt and Swift are doing much more work than a C compiler would? The analysis necessary for the borrow checker is not free, likewise with a lot of other compile-time checks in both languages. C can be fast because it effectively does no compile-time checking of things beyond basic syntax so you can call foo(char) with foo(int) and other unholy things.
The borrow checker is usually a blip on the overall graph of compilation time.
The overall principle is sound though: it's true that doing some work is more than doing no work. But the borrow checker and other safety checks are not the root of compile time performance in Rust.
These languages do more at compile time, yes. However, I learned from Ryan's discord server that he did a unity build in a C++ codebase and got similar results (just a few seconds slower than the C code). Also, you could see in the article that most of the time was being spent in LLVM and linking. With a unity build, you nearly cut out link step entirely. Rust and Swift do some sophisticated things (hinley-milner, generics, etc.) but I have my doubts that those things cause the most slowdown.
That’s not a good example. Foo(int) is analyzed by compiler and a type conversion is inserted.
The language spec might be bad, but this isn’t letting the compiler cut corners.
This explanation gets repeated over and over again in discussions about the speed of the Rust compiler, but apart from rare pathological cases, the majority of time in a release build is not spent doing compile-time checks, but in LLVM. Rust has zero-cost abstractions, but the zero-cost refers to runtime, sadly there's a lot of junk generated at compile-time that LLVM has to work to remove. Which is does, very well, but at cost of slower compilation.
I don't think it's interesting to observe that C code can be compiled quickly (so can Go, a language designed specifically for fast compilation). It's not a problem intrinsic to compilation; the interesting hard problem is to make Rust's semantics compile quickly. This is a FAQ on the Rust website.
I encountered one project in 2000-th with few dozens of KLoC in C++. It compiled in a fraction of a second on old computer. My hello world code with Boost took few seconds to compile. So it's not just about language, it's about structuring your code and using features with heavy compilation cost. I'm pretty sure that you can write Doom with C macros and it won't be fast. I'm also pretty sure, that you can write Rust code in a way to compile very fast.
I'd be very interested to see a list of features/patterns and the cost that they incur on the compiler. Ideally, people should be able to use the whole language without having to wait so long for the result.
My anecdata would be that the average C++ developer puts includes inside of every header file which includes more headers to the point where everything is including everything else and a single .cpp file draws huge swaths of unnecessary code in and the project takes eons to compile on a fast computer.
That's my 2000s development experience. Fortunately I've spent a good chunk of the 2010s and most of the 2020s using other languages.
The classic XKCD compilation comic exists for a reason.
I bet that if you take those 278k lines of code and rewrite them in simple Rust, without using generics, or macros, and using a single crate, without dependencies, you could achieve very similar compile times. The Rust compiler can be very fast if the code is simple. It's when you have dependencies and heavy abstractions (macros, generics, traits, deep dependency trees) that things become slow.
I'm curious about that point you made about dependencies. This Rust project (https://github.com/microsoft/edit) is made with essentially no dependencies, is 17,426 lines of code, and on an M4 Max it compiles in 1.83s debug and 5.40s release. The code seems pretty simple as well.
Edit: Note also that this is 10k more lines than the OP's project. This certainly makes those deps suspicious.
Every claim I've seen about unity builds being fast just never rings true to me. I just downloaded the rad debugger and ran the build script on a 7950x (about as fast as you can get). A debug build took 5s, a release build 34s with either gcc or clang.
Maybe it's a MSVC thing - it does seem to have some multi-threading stuff. In any case raddbg non-clean builds take longer than any of my rust projects.
I use unity builds day in day out. The speed up is an order of magnitude on a 2m+ LOC project.
If you want to see the difference download unreal engine and compile the editor with and without unity builds enabled.
My experience has been the polar opposite of yours - similar size rust projects are an order of magnitude slower than C++ ones. Could you share an example of a project to compare with?
> makes me wonder why other languages like Rust and Swift can't just do something similar to achieve similar speeds.
One of the primary features of Rust is the extensive compile-time checking. Monomorphization is also a complex operation, which is not exclusive to Rust.
C compile times should be very fast because it's a relatively low-level language.
On the grand scale of programming languages and their compile-time complexity, C code is closer to assembly language than modern languages like Rust or Swift.
That "just" was too flippant. My bad. What I meant to convey is "hey, there's some fast compiling going on here and it wasn't that hard to pull off. Can we at least take a look at why that is and maybe do the same thing?".
I guess you can do that, but if for some reason you needed to compile separately, (suppose you sell the system to a third party to a client, and they need to modify module 1, module 2 and the main loop.)
It would be pretty trivial to remove some #include "module3.c" lines and add some -o module3 options to the compiler. Right?
I'm not sure what Rust or docker have to do with this basic issue, it just feels like young blood attempting 2020 solutions before exploring 1970 solutions.
There's also Jonathan Blow's jai where he routinely builds an entire game from scratch in a few seconds (hopefully public beta will be released by the end of this year).
Rust is doing a lot more under the hood. C doesn't track variable lifetimes, ownership, types, generics, handle dependency management, or handle compile-time execution (beyond the limited language that is the pre-compiler). The rust compiler also makes intelligent (scary intelligent!) suggestions when you've made a mistake: it needs a lot of context to be able to do that.
The rust compiler is actually pretty fast for all the work it's doing. It's just an absolutely insane amount of additional work. You shouldn't expect it to compile as fast as C.
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.
> 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
Well, that point in the design space was already occupied by Java which also has extremely fast builds. Go exists primarily because the designers wanted to make a new programming language, as far as I can tell. It has some nice implementation aspects but it picked up its users mostly from the Python/Ruby/JS world rather than C/C++/Java, which was the original target market they had in mind (i.e. Google servers). Scripting language users were in the market for a language that had a type system but not one that was too advanced, and which kept the scripting "feel" of very fast turnaround times. But not Java because that was old and unhip, and all the interesting intellectual space like writing libs/conf talks was camped on already.
Java still had slow startup and warmup time circa 2005-2007, on the order of 1-3 seconds for hello world and quite a bit more for real apps. That is horrendous for anything CLI based.
And you left out classloader/classpath/JAR dependency hell, which was horrid circa late 90s/early 2000s...and I'm guessing was still a struggle when Go really started development. Especially at Google's scale.
Don't get me wrong, Java has come a long way and is a fine language and the JVM is fantastic. But the java of 2025 is not the same as mid-to-late 2000s.
> 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.
I'm a fan of Go, but I don't think it's the product of some awesome collective Google wisdom and experience. Had it been, I think they'd have come to the conclusion that statically eliminating null pointer exceptions was a worthwhile endeavor, just to mention one thing. Instead, I think it's just the product of some people at Google making a language they way they wanted to.
That is exactly what go was meant for and there is nothing better than picking the right tool for the job. The only foot gun I have seen people run into is parallelism with mutable shared state through channels can be subtly and exploitably wrong. I don't feel like most people use channels like that though? I use rust because that isn't the job I have. I usually have to cramb slow algorithms into slower hardware, and the problems are usually almost but not quite embarrassingly parallel.
I think a lot of the materials that the Go folks put out in the early days encourage a very channel-heavy style of programming that leads to extremely bad places.
Nowadays the culture seems to have evolved a bit. I now go into high alert mode if I see a channel cross a function boundary or a goroutine that wasn't created via errgroup or similar.
People also seem to have chilled out about the "share by communicating" thing. It's usually better to just use a mutex and I think people recognise that now.
You can have the best of both worlds: A fast, but sloppy compiler and slow, but thorough checkers/linters. I think it's ideal that way, but rust seems to have chosen to needlessly combine both actions into one.
What are obnoxious types? Types either represent the data correctly or not. I think you can force types to shut up the compiler in any language including Haskell, Idris, PureScript...
I'd say you already get like 70% of the benefit of a type system with just the basic "you can't pass an int where string is expected". Being able to define your own types based on the basic ones, like "type Email string", so it's no longer possible to pass a "string" where "Email" is expected gets you to 80%. Add Result and Optional types (or arguably just sum types if you prefer) and you're at 95%. Anything more and you're pushing into diminishing returns.
This might work for the types you create, but what about all the code written in the language that expects the “proper” structure?
> Types either represent the data or not
This definitely required, but is only really the first step. Where types get really useful is when you need to change them later on. The key aspects here are how easily you can change them, and how much the language tooling can help.
Also strikes me as not fully understanding what exactly docker is doing. In reference to building everything in a docker image:
"Unfortunately, this will rebuild everything from scratch whenever there's any change."
In this situation, with only one person as the builder, with no need for CI or CD or whatever, there's nothing wrong with building locally with all the local conveniences and just slurping the result into a docker container. Double-check any settings that may accidentally add paths if the paths have anything that would bother you. (In my case it would merely reveal that, yes, someone with my username built it and they have a "src" directory... you can tell how worried I am about both those tidbits by the fact I just posted them publicly.)
It's good for CI/CD in a professional setting to ensure that you can build a project from a hard drive, a magnetic needle, and a monkey trained to scratch a minimal kernel on to it, and boot strap from there, but personal projects don't need that.
Thank you! I got a couple minutes in and was confused as hell. There is no reason to do the builds in the container.
Even at work, I have a few projects where we had to build a Java uber jar (all the dependencies bundled into one big far) and when we need it containerized we just copy the jar in.
I honestly don't see much reason to do builds in the container unless there is some limitation in my CICD pipeline where I don't have access to necessary build tools.
Half the point of containerization is to have reproducible builds. You want a build environment that you can trust will be identical 100% of the time. Your host machine is not that. If you run `pacman -Syu`, you no longer have the same build environment as you did earlier.
If you now copy your binary to the container and it implicitly expects there to be a shared library in /usr/lib or wherever, it could blow up at runtime because of a library version mismatch.
From the article, the goal was not to simplify, but rather to modernize:
> So instead, I'd like to switch to deploying my website with containers (be it Docker, Kubernetes, or otherwise), matching the vast majority of software deployed any time in the last decade.
Containers offer many benefits. To name some: process isolation, increased security, standardized logging and mature horizontal scalability.
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.
I also don’t understand this, the rust compiler hardly bothers me at all when I’m working. I feel like this is due to how bad it was early on and people just sticking to that narrative
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
With a little bit of vimrc magic you can make it transparent:
"Make k/j up/down work more naturally by going to the next displayed line vs
"going to the next logical line (for when word-wrapping is on):
noremap k gk
noremap j gj
noremap <up> gk
noremap <down> gj
"Same as above, but for arrow keys in insert mode:
inoremap <up> <Esc>gka
inoremap <down> <Esc>gja
Just like every submission about C/C++ gets a comment about how great Rust is, every submission about Rust gets a comment about how great Zig is. Like a clockwork.
Edit: apparently I am replying to the main Zig author? Language evangelism is by far the worst part of Rust and has likely stirred up more anti Rust sentiment than “converting” people to Rust. If you truly care for your language you should use whatever leverage you have to steer your community away from evangelism, not embrace it.
This comment would be a lot better if it engaged with the posted article, or really had any sort of insight beyond a single compile time metric. What do you want me to take away from your comment? Zig good and Rust bad?
@AndyKelley I'm super curious what you think the main factors are that make languages like Zig super fast at compiling where languages like Rust and Swift are quite slow. What's the key difference?
I'm not Andrew, but Rust has made several language design decisions that make compiler performance difficult. Some aspects of compiler speed come down to that.
One major difference is the way each project considers compiler performance:
The Rust team has always cared to some degree about this. But, from my recollection of many RFCs, "how does this impact compiler performance" wasn't a first-class concern. And that also doesn't really speak to a lot of the features that were basically implemented before the RFC system existed. So while it's important, it's secondary to other things. And so while a bunch of hard-working people have put in a ton of work to improve performance, they also run up against these more fundamental limitations at the limit.
Andrew has pretty clearly made compiler performance a first-class concern, and that's affected language design decisions. Naturally this leads to a very performant compiler.
Basically, not depending on LLVM or LLD. The above is only possible because we invested years into making our own x86_64 backend and our own linker. You can see all the people ridiculing this decision 2 years ago https://news.ycombinator.com/item?id=36529456
I'm also curious because I've (recently) compiled more or less identical programs in Zig and Rust and they took the same amount of time to compile. I'm guessing people are just making Zig programs with less code and fewer dependencies and not really comparing apples to apples.
My non-static Rust website (includes an actual webserver as well as a react-like framework for templating) takes 1.25s to do an incremental recompile with "cargo watch" (which is an external watcher that just kills the process and reruns "cargo run").
And it can be considerably faster if you use something like subsecond[0] (which does incremental linking and hotpatches the running binary). It's not quite as fast as Zig, but it's close.
However, if that 331ms build above is a clean (uncached) build then that's a lot faster than a clean build of my website which takes ~12s.
The 331ms time is mostly uncached. In this case the build script was already cached (must be re-done if the build script is edited), and compiler_rt was already cached (must be done exactly once per target; almost never rebuilt).
It isn't a lot of things, but I would argue that its exceptionally (heh) good exception handling model / philosophy (making it good, required, and performant) is more important than memory safety, especially when a lot of performance-oriented / bit-banging Rust code just gets shoved into Unsafe blocks anyway. Even C/C++ can be made memory safe, cf. https://github.com/pizlonator/llvm-project-deluge
What I'm more interested to know is what the runtime performance tradeoff is like now; one really has to assume that it's slower than LLVM-generated code, otherwise that monumental achievement seems to have somehow been eclipsed in very short time, with much shorter compile times to boot.
Zig is a small and simple language. It doesn't need a complicated compiler.
Rust is a large and robust language meant for serious systems programming. The scope of problems Rust addresses is large, and Rust seeks to be deployed to very large scale software problems.
These two are not the same and do not merit an apples to apples comparison.
edit: I made some changes to my phrasing. I described Zig as a "toy" language, which wasn't the right wording.
These languages are at different stages of maturity, have different levels of complexity, and have different customers. They shouldn't be measured against each other so superficially.
This is an amusing argument to make in favor of Rust, since it's exactly the kind of dismissive statement that Ada proponents make about other languages including Rust.
(EDIT: The parent has since edited this comment to contain more than just "zig bad rust good", but I still think the combative-ness and insulting tone at the time I made this comment isn't cool.)
Which is one of the reasons why Rust is considered to be targeting C++'s developers. C++ devs already have the Stockholm syndrome needed to tolerate the tooling.
Also modern c++ with value semantics is more functional than many other languages people might come to rust from, that keeps the borrow checker from being as annoying. If people are used to making webs of stateful classes with references to each pther. The borrow checker is horrific, but that is because that design pattern is horrific if you multithread it.
Things can still be slow in absolute terms without being as slow as C++. The issues with compiling C++ are incredibly well understood and documented. It is one of the worst languages on earth for compile times. Rust doesn’t share those language level issues, so the expectations are understandably higher.
But it does share some of those issues. Specifically, while Rust generics aren't as unstructured as C++ templates, the main burden is actually from compiling all those tiny instantiations, and Rust monomorphization has the same exact problem responsible for the bulk of its compile times.
Rust shares pretty much every language-level issue C++ has with compile times, no? Monomorphization explosion, turing-complete compile time macros, complex type system.
I thorougly enjoy all the work on encapsulation and reducing the steps of compilation to compile, then link that C does... Only to have C++ come along and undo almost all of it through the simple expedient of requiring templates for everything.
Oops, changed one template in one header. And that impacts.... 98% of my code.
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.
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.
Actually JITs can be faster than AOT compilation because they can be optimized for the current architecture they are running in. There were claims Julia, a JIT language can beat C in some benchmarks
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.
I participated in the most recent Bevy game jam and the community has a new tool that came out of Dioxus called subsecond which as the name suggests provides sub-second hot reloading of systems. It made prototyping very pleasant. Especially when iterating on UI.
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.
Yeah, for application code in my experience the more I stick to the dumb way to do it the less I fight the borrow checker along with fewer trait issues.
Refactoring seems to take about the same time too so no loss on that front. After all is said and done I'm just left with various logic bugs to fix which is par for the course (at least for me) and a sense of wondering if I actually did everything properly.
I suppose maybe two years from now we'll have people that suggest avoiding generics and tempering macro usage. These days most people have heard the advice about not stressing over cloning and unwraping (though expect is much better imo) on the first pass more or less.
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.
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.
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.
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.
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.
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.
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.
What "most" cases are you thinking of? Also don't forget that a binary that in release weights 10 MB, when compiled with debug symbols can weight 300 MB, which is way less practical to distribute.
>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.
The problem has been created by Docker which destroys all of the state. If this was C, you'd also end up losing all of the object files and rebuilding them every time.
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.
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).
Rust can and does compile separate translation units in parallel; it's just that the translation unit is (roughly) a crate instead of a single C or C++ source file.
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.
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.
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.
On the other hand you get mentally insane if you try to work in a way that you do s.th. usefull during the 5-10 min compile times you often have with C++ projects.
When I had to deal with this I would just open the newspaper and read an article in front of my boss.
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.
It would be more accurate to say that idiomatic Rust encourages doing things which are slow to compile: lots of small generic functions everywhere. And the most effective way to speed this up is to avoid monomorphization by using RTTI to provide a single generic compiled implementation that can be reused for different types, like what Swift does when generics across the module boundary. But this is less efficient at runtime because of all the runtime checks and computations that now need to be done to deal with objects of different sizes etc, many direct or even inlined calls now become virtual etc.
It's starting to, but a lot of people are using Rust because they need (or want) the best possible runtime performance, so that tends to be prioritised a lot of the time.
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.
As for templates, I can't think of anything about them that would speed up things substantially wrt Rust aside from extern template and manually managing your instantiations in separate .cpp files. Since otherwise it's fundamentally the same problem - recompiling the same code over and over again because it's parametrized with different types every time.
Indeed, out of the box I would actually expect C++ to do worse because a C++ header template has potentially different environment in every translation unit in which that header is included, so without precompiled headers the compiler pretty much has to assume the worst...
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.
The more your compiler does for you at build time, the longer it will take to build, it's that simple.
Go has sub-second build times even on massive code-bases. Why? because it doesn't do a lot at build time. It has a simple module system, (relatively) simple type system, and leaves a whole bunch of stuff be handled by the GC at runtime. It's great for its intended use case.
When you have things like macros, advanced type systems, and want robustness guarantees at build time.. then you have to pay for that.
I think this is mostly a myth. If you look at Rust compiler benchmarks, while typechecking isn't _free_ it's also not the bottleneck.
A big reason that amalgamation builds of C and C++ can absolutely fly is because they aren't reparsing headers and generating exactly one object file so the linker has no work to do.
Once you add static linking to the toolchain (in all of its forms) things get really fucking slow.
Codegen is also a problem. Rust tends to generate a lot more code than C or C++, so while the compiler is done doing most of its typechecking work, the backend and assembler has a lot of things to chuck through.
7 replies →
That the type system is responsible for rust's slow builds is a common and enduring myth. `cargo check` (which just does typechecking) is actually usually pretty fast. Most of the build time is spent in the code generation phase. Some macros do cause problems as you mention, since the code that contains the macro must be compiled before the code that uses it, so they reduce parallelism.
5 replies →
Yes but I'd also add that Go specifically does not optimize well.
The compiler is optimized for compilation speed, not runtime performance. Generally speaking, it does well enough. Especially because it's usecase is often applications where "good enough" is good enough (IE, IO heavy applications).
You can see that with "gccgo". Slower to compile, faster to run.
2 replies →
> Go has sub-second build times even on massive code-bases.
Unless you use sqlite, in which case your build takes a million years.
Not really. The root reason behind Go's fast compilation is that it was specifically designed to compile fast. The implementation details are just a natural consequence of that design decision.
Since fast compilation was a goal, every part of the design was looked at through a rough "can this be a horrible bottleneck?", and discarded if so. For example, the import (package) system was designed to avoid the horrible, inefficient mess of C++. It's obvious that you never want to compile the same package more than once and that you need to support parallel package compilation. These may be blindingly obvious, but if you don't think about compilation speed at design time, you'll get this wrong and will never be able to fix it.
As far as optimizations vs compile speed goes, it's just a simple case of diminishing returns. Since Rust has maximum possible perfomance as a goal, it's forced to go well into the diminishing returns territory, sacrificing a ton of compile speed for minor performance improvements. Go has far more modest performance goals, so it can get 80% of the possible performance for only 20% of the compile cost. Rust can't afford to relax its stance because it's competing with languages like C++, and to some extent C, that are willing to go to any length to squeeze out an extra 1% of perfomance.
Dlang compilers does more than any C++ compiler (metaprogramming, a better template system and compile time execution) and it's hugely faster. Language syntax design has a role here.
This is sometimes called amalgamation and you can do it Rust as well. Either manually or with tools. The point is that apart from very specific niches it is just not a practical approach.
It's not that it can't be done but that it usually is not worth the hassle and our goal should be for compilation to be fast despite not everything being in one file.
Turbo Pascal is a prime example for a compiler that won the market not least because of its - for the time - outstanding compilation speed.
In the same vein, a language can be designed for fast compilation. Pascal in general was designed for single-pass compilation which made it naturally fast. All the necessary forward declarations were a pain though and the victory of languages that are not designed for single-pass compilation proofs that while doable it was not worth it in the end.
Because Russt and Swift are doing much more work than a C compiler would? The analysis necessary for the borrow checker is not free, likewise with a lot of other compile-time checks in both languages. C can be fast because it effectively does no compile-time checking of things beyond basic syntax so you can call foo(char) with foo(int) and other unholy things.
The borrow checker is usually a blip on the overall graph of compilation time.
The overall principle is sound though: it's true that doing some work is more than doing no work. But the borrow checker and other safety checks are not the root of compile time performance in Rust.
2 replies →
These languages do more at compile time, yes. However, I learned from Ryan's discord server that he did a unity build in a C++ codebase and got similar results (just a few seconds slower than the C code). Also, you could see in the article that most of the time was being spent in LLVM and linking. With a unity build, you nearly cut out link step entirely. Rust and Swift do some sophisticated things (hinley-milner, generics, etc.) but I have my doubts that those things cause the most slowdown.
That’s not a good example. Foo(int) is analyzed by compiler and a type conversion is inserted. The language spec might be bad, but this isn’t letting the compiler cut corners.
If you'd like the rust compiler to operate quickly:
* Make no nested types - these slow compiler time a lot
* Include no crates, or ones that emphasize compiler speed
C is still v. fast though. That's why I love it (and Rust).
1 reply →
This explanation gets repeated over and over again in discussions about the speed of the Rust compiler, but apart from rare pathological cases, the majority of time in a release build is not spent doing compile-time checks, but in LLVM. Rust has zero-cost abstractions, but the zero-cost refers to runtime, sadly there's a lot of junk generated at compile-time that LLVM has to work to remove. Which is does, very well, but at cost of slower compilation.
6 replies →
I don't think it's interesting to observe that C code can be compiled quickly (so can Go, a language designed specifically for fast compilation). It's not a problem intrinsic to compilation; the interesting hard problem is to make Rust's semantics compile quickly. This is a FAQ on the Rust website.
I encountered one project in 2000-th with few dozens of KLoC in C++. It compiled in a fraction of a second on old computer. My hello world code with Boost took few seconds to compile. So it's not just about language, it's about structuring your code and using features with heavy compilation cost. I'm pretty sure that you can write Doom with C macros and it won't be fast. I'm also pretty sure, that you can write Rust code in a way to compile very fast.
I'd be very interested to see a list of features/patterns and the cost that they incur on the compiler. Ideally, people should be able to use the whole language without having to wait so long for the result.
6 replies →
My anecdata would be that the average C++ developer puts includes inside of every header file which includes more headers to the point where everything is including everything else and a single .cpp file draws huge swaths of unnecessary code in and the project takes eons to compile on a fast computer.
That's my 2000s development experience. Fortunately I've spent a good chunk of the 2010s and most of the 2020s using other languages.
The classic XKCD compilation comic exists for a reason.
My C compiler, which is pretty naive and around ~90,000 lines, can compile _itself_ in around 1 second. Clang can do it in like 0.4.
The simple truth is a C compiler doesn’t need to do very much!
I bet that if you take those 278k lines of code and rewrite them in simple Rust, without using generics, or macros, and using a single crate, without dependencies, you could achieve very similar compile times. The Rust compiler can be very fast if the code is simple. It's when you have dependencies and heavy abstractions (macros, generics, traits, deep dependency trees) that things become slow.
I'm curious about that point you made about dependencies. This Rust project (https://github.com/microsoft/edit) is made with essentially no dependencies, is 17,426 lines of code, and on an M4 Max it compiles in 1.83s debug and 5.40s release. The code seems pretty simple as well. Edit: Note also that this is 10k more lines than the OP's project. This certainly makes those deps suspicious.
1 reply →
I can't help but think the borrow checker alone would slow this down by at least 1 or 2 orders of magnitude.
3 replies →
Every claim I've seen about unity builds being fast just never rings true to me. I just downloaded the rad debugger and ran the build script on a 7950x (about as fast as you can get). A debug build took 5s, a release build 34s with either gcc or clang.
Maybe it's a MSVC thing - it does seem to have some multi-threading stuff. In any case raddbg non-clean builds take longer than any of my rust projects.
I use unity builds day in day out. The speed up is an order of magnitude on a 2m+ LOC project.
If you want to see the difference download unreal engine and compile the editor with and without unity builds enabled.
My experience has been the polar opposite of yours - similar size rust projects are an order of magnitude slower than C++ ones. Could you share an example of a project to compare with?
Alpha. Windows-only.
https://codeload.github.com/EpicGamesExt/raddebugger/tar.gz/...
That is kind of surprising. The sqlite "unity" build, has about the same number of lines of C and takes a lot longer than that to compile.
> makes me wonder why other languages like Rust and Swift can't just do something similar to achieve similar speeds.
One of the primary features of Rust is the extensive compile-time checking. Monomorphization is also a complex operation, which is not exclusive to Rust.
C compile times should be very fast because it's a relatively low-level language.
On the grand scale of programming languages and their compile-time complexity, C code is closer to assembly language than modern languages like Rust or Swift.
"Just". Probably because there's a lot of complexity you're waving away. Almost nothing is ever simple as "just".
At a previous company, we had a rule: whoever says "just" gets to implement it :)
1 reply →
That "just" was too flippant. My bad. What I meant to convey is "hey, there's some fast compiling going on here and it wasn't that hard to pull off. Can we at least take a look at why that is and maybe do the same thing?".
4 replies →
I guess you can do that, but if for some reason you needed to compile separately, (suppose you sell the system to a third party to a client, and they need to modify module 1, module 2 and the main loop.) It would be pretty trivial to remove some #include "module3.c" lines and add some -o module3 options to the compiler. Right?
I'm not sure what Rust or docker have to do with this basic issue, it just feels like young blood attempting 2020 solutions before exploring 1970 solutions.
C hardly requires any high effort compile things. No templates, no generics, super simple types, no high level structures.
There's also Jonathan Blow's jai where he routinely builds an entire game from scratch in a few seconds (hopefully public beta will be released by the end of this year).
Rust is doing a lot more under the hood. C doesn't track variable lifetimes, ownership, types, generics, handle dependency management, or handle compile-time execution (beyond the limited language that is the pre-compiler). The rust compiler also makes intelligent (scary intelligent!) suggestions when you've made a mistake: it needs a lot of context to be able to do that.
The rust compiler is actually pretty fast for all the work it's doing. It's just an absolutely insane amount of additional work. You shouldn't expect it to compile as fast as C.
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.
> 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
Well, that point in the design space was already occupied by Java which also has extremely fast builds. Go exists primarily because the designers wanted to make a new programming language, as far as I can tell. It has some nice implementation aspects but it picked up its users mostly from the Python/Ruby/JS world rather than C/C++/Java, which was the original target market they had in mind (i.e. Google servers). Scripting language users were in the market for a language that had a type system but not one that was too advanced, and which kept the scripting "feel" of very fast turnaround times. But not Java because that was old and unhip, and all the interesting intellectual space like writing libs/conf talks was camped on already.
Java still had slow startup and warmup time circa 2005-2007, on the order of 1-3 seconds for hello world and quite a bit more for real apps. That is horrendous for anything CLI based.
And you left out classloader/classpath/JAR dependency hell, which was horrid circa late 90s/early 2000s...and I'm guessing was still a struggle when Go really started development. Especially at Google's scale.
Don't get me wrong, Java has come a long way and is a fine language and the JVM is fantastic. But the java of 2025 is not the same as mid-to-late 2000s.
> 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.
I'm a fan of Go, but I don't think it's the product of some awesome collective Google wisdom and experience. Had it been, I think they'd have come to the conclusion that statically eliminating null pointer exceptions was a worthwhile endeavor, just to mention one thing. Instead, I think it's just the product of some people at Google making a language they way they wanted to.
One day I would like to just change pascals syntax a bit to be Pythonic and just blow the socks of junior and Go developers.
That is exactly what go was meant for and there is nothing better than picking the right tool for the job. The only foot gun I have seen people run into is parallelism with mutable shared state through channels can be subtly and exploitably wrong. I don't feel like most people use channels like that though? I use rust because that isn't the job I have. I usually have to cramb slow algorithms into slower hardware, and the problems are usually almost but not quite embarrassingly parallel.
I think a lot of the materials that the Go folks put out in the early days encourage a very channel-heavy style of programming that leads to extremely bad places.
Nowadays the culture seems to have evolved a bit. I now go into high alert mode if I see a channel cross a function boundary or a goroutine that wasn't created via errgroup or similar.
People also seem to have chilled out about the "share by communicating" thing. It's usually better to just use a mutex and I think people recognise that now.
You can have the best of both worlds: A fast, but sloppy compiler and slow, but thorough checkers/linters. I think it's ideal that way, but rust seems to have chosen to needlessly combine both actions into one.
What are obnoxious types? Types either represent the data correctly or not. I think you can force types to shut up the compiler in any language including Haskell, Idris, PureScript...
I'd say you already get like 70% of the benefit of a type system with just the basic "you can't pass an int where string is expected". Being able to define your own types based on the basic ones, like "type Email string", so it's no longer possible to pass a "string" where "Email" is expected gets you to 80%. Add Result and Optional types (or arguably just sum types if you prefer) and you're at 95%. Anything more and you're pushing into diminishing returns.
1 reply →
This might work for the types you create, but what about all the code written in the language that expects the “proper” structure?
> Types either represent the data or not
This definitely required, but is only really the first step. Where types get really useful is when you need to change them later on. The key aspects here are how easily you can change them, and how much the language tooling can help.
Is Go still in heavy use at Google these days?
What would they use for networking if not Go?
2 replies →
Related from a couple of weeks ago:
https://news.ycombinator.com/item?id=44234080
(Rust compiler performance; 287 points, 261 comments)
That person seems to be confused. Installing a single, statically linked binary is clearly simpler than managing a container?!
Also strikes me as not fully understanding what exactly docker is doing. In reference to building everything in a docker image:
"Unfortunately, this will rebuild everything from scratch whenever there's any change."
In this situation, with only one person as the builder, with no need for CI or CD or whatever, there's nothing wrong with building locally with all the local conveniences and just slurping the result into a docker container. Double-check any settings that may accidentally add paths if the paths have anything that would bother you. (In my case it would merely reveal that, yes, someone with my username built it and they have a "src" directory... you can tell how worried I am about both those tidbits by the fact I just posted them publicly.)
It's good for CI/CD in a professional setting to ensure that you can build a project from a hard drive, a magnetic needle, and a monkey trained to scratch a minimal kernel on to it, and boot strap from there, but personal projects don't need that.
Thank you! I got a couple minutes in and was confused as hell. There is no reason to do the builds in the container.
Even at work, I have a few projects where we had to build a Java uber jar (all the dependencies bundled into one big far) and when we need it containerized we just copy the jar in.
I honestly don't see much reason to do builds in the container unless there is some limitation in my CICD pipeline where I don't have access to necessary build tools.
1 reply →
Half the point of containerization is to have reproducible builds. You want a build environment that you can trust will be identical 100% of the time. Your host machine is not that. If you run `pacman -Syu`, you no longer have the same build environment as you did earlier.
If you now copy your binary to the container and it implicitly expects there to be a shared library in /usr/lib or wherever, it could blow up at runtime because of a library version mismatch.
From the article, the goal was not to simplify, but rather to modernize:
> So instead, I'd like to switch to deploying my website with containers (be it Docker, Kubernetes, or otherwise), matching the vast majority of software deployed any time in the last decade.
Containers offer many benefits. To name some: process isolation, increased security, standardized logging and mature horizontal scalability.
So put the binary in the container. Why does it have to be compiled within the container?
6 replies →
Mightily resisting the urge to be flippant, but all of those benefits were achieved before Docker.
Docker is a (the, in some areas) modern way to do it, but far from the only way.
Increased security compared to bare hardware, lower than VMs. Also, lower than Jails and RKT (Rocket) which seems to be dead.
> process isolation, increased security
no, that's sandboxing.
Exactly. I immediately thought of the grug brain dev when I read that.
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.
I also don’t understand this, the rust compiler hardly bothers me at all when I’m working. I feel like this is due to how bad it was early on and people just sticking to that narrative
When C++ templates are turing complete is it pointless to complain about the compile times without considering the actual code :)
> 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
With a little bit of vimrc magic you can make it transparent:
My homepage takes 73ms to rebuild: 17ms to recompile the static site generator, then 56ms to run it.
Just like every submission about C/C++ gets a comment about how great Rust is, every submission about Rust gets a comment about how great Zig is. Like a clockwork.
Edit: apparently I am replying to the main Zig author? Language evangelism is by far the worst part of Rust and has likely stirred up more anti Rust sentiment than “converting” people to Rust. If you truly care for your language you should use whatever leverage you have to steer your community away from evangelism, not embrace it.
Neat, I guess?
This comment would be a lot better if it engaged with the posted article, or really had any sort of insight beyond a single compile time metric. What do you want me to take away from your comment? Zig good and Rust bad?
I think the most relevant thing is that building a simple website can (and should) take milliseconds, not minutes, and that -- quoting from the post:
> A brief note: 50 seconds is fine, actually!
50 seconds should actually not be considered fine.
3 replies →
@AndyKelley I'm super curious what you think the main factors are that make languages like Zig super fast at compiling where languages like Rust and Swift are quite slow. What's the key difference?
I'm not Andrew, but Rust has made several language design decisions that make compiler performance difficult. Some aspects of compiler speed come down to that.
One major difference is the way each project considers compiler performance:
The Rust team has always cared to some degree about this. But, from my recollection of many RFCs, "how does this impact compiler performance" wasn't a first-class concern. And that also doesn't really speak to a lot of the features that were basically implemented before the RFC system existed. So while it's important, it's secondary to other things. And so while a bunch of hard-working people have put in a ton of work to improve performance, they also run up against these more fundamental limitations at the limit.
Andrew has pretty clearly made compiler performance a first-class concern, and that's affected language design decisions. Naturally this leads to a very performant compiler.
3 replies →
Basically, not depending on LLVM or LLD. The above is only possible because we invested years into making our own x86_64 backend and our own linker. You can see all the people ridiculing this decision 2 years ago https://news.ycombinator.com/item?id=36529456
6 replies →
I'm also curious because I've (recently) compiled more or less identical programs in Zig and Rust and they took the same amount of time to compile. I'm guessing people are just making Zig programs with less code and fewer dependencies and not really comparing apples to apples.
5 replies →
My non-static Rust website (includes an actual webserver as well as a react-like framework for templating) takes 1.25s to do an incremental recompile with "cargo watch" (which is an external watcher that just kills the process and reruns "cargo run").
And it can be considerably faster if you use something like subsecond[0] (which does incremental linking and hotpatches the running binary). It's not quite as fast as Zig, but it's close.
However, if that 331ms build above is a clean (uncached) build then that's a lot faster than a clean build of my website which takes ~12s.
[0]: https://news.ycombinator.com/item?id=44369642
The 331ms time is mostly uncached. In this case the build script was already cached (must be re-done if the build script is edited), and compiler_rt was already cached (must be done exactly once per target; almost never rebuilt).
1 reply →
Nice. Didn't realize zig build has --watch and -fincremental added. I was mostly using "watchexec -e zig zig build" for recompile on file changes.
New to 0.14.0!
Zig isn’t memory safe though right?
It isn't a lot of things, but I would argue that its exceptionally (heh) good exception handling model / philosophy (making it good, required, and performant) is more important than memory safety, especially when a lot of performance-oriented / bit-banging Rust code just gets shoved into Unsafe blocks anyway. Even C/C++ can be made memory safe, cf. https://github.com/pizlonator/llvm-project-deluge
What I'm more interested to know is what the runtime performance tradeoff is like now; one really has to assume that it's slower than LLVM-generated code, otherwise that monumental achievement seems to have somehow been eclipsed in very short time, with much shorter compile times to boot.
3 replies →
How confident are you that memory safety (or lack thereof) is a significant variable in how fast a compiler is?
Zig is less memory safe than Rust, but more than C/C++. Neither Zig nor Rust is fundamentally memory safe.
11 replies →
Zig is a small and simple language. It doesn't need a complicated compiler.
Rust is a large and robust language meant for serious systems programming. The scope of problems Rust addresses is large, and Rust seeks to be deployed to very large scale software problems.
These two are not the same and do not merit an apples to apples comparison.
edit: I made some changes to my phrasing. I described Zig as a "toy" language, which wasn't the right wording.
These languages are at different stages of maturity, have different levels of complexity, and have different customers. They shouldn't be measured against each other so superficially.
This is an amusing argument to make in favor of Rust, since it's exactly the kind of dismissive statement that Ada proponents make about other languages including Rust.
Come on now. This isn't acceptable behavior.
(EDIT: The parent has since edited this comment to contain more than just "zig bad rust good", but I still think the combative-ness and insulting tone at the time I made this comment isn't cool.)
5 replies →
As a former C++ developer, claims that rust compilation is slow leave me scratching my head.
Which is one of the reasons why Rust is considered to be targeting C++'s developers. C++ devs already have the Stockholm syndrome needed to tolerate the tooling.
Rust's compilation is slow, but the tooling is just about the best that any programming language has.
7 replies →
Also modern c++ with value semantics is more functional than many other languages people might come to rust from, that keeps the borrow checker from being as annoying. If people are used to making webs of stateful classes with references to each pther. The borrow checker is horrific, but that is because that design pattern is horrific if you multithread it.
> Stockholm syndrome
A.k.a. "Remember the Vasa!" https://news.ycombinator.com/item?id=17172057
Things can still be slow in absolute terms without being as slow as C++. The issues with compiling C++ are incredibly well understood and documented. It is one of the worst languages on earth for compile times. Rust doesn’t share those language level issues, so the expectations are understandably higher.
But it does share some of those issues. Specifically, while Rust generics aren't as unstructured as C++ templates, the main burden is actually from compiling all those tiny instantiations, and Rust monomorphization has the same exact problem responsible for the bulk of its compile times.
Rust shares pretty much every language-level issue C++ has with compile times, no? Monomorphization explosion, turing-complete compile time macros, complex type system.
1 reply →
Classic case of:
New features: yes
Talking to users and fixing actual problems: lolno, I CBF
I thorougly enjoy all the work on encapsulation and reducing the steps of compilation to compile, then link that C does... Only to have C++ come along and undo almost all of it through the simple expedient of requiring templates for everything.
Oops, changed one template in one header. And that impacts.... 98% of my code.
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.
The intermediates for my project are 150GB+ alone. Last time I worked with docker images that large we had massive massive problems.
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.
Actually JITs can be faster than AOT compilation because they can be optimized for the current architecture they are running in. There were claims Julia, a JIT language can beat C in some benchmarks
In fact, JITs can be faster because they can specialize code, i.e. make optimizations based on live data.
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.
I participated in the most recent Bevy game jam and the community has a new tool that came out of Dioxus called subsecond which as the name suggests provides sub-second hot reloading of systems. It made prototyping very pleasant. Especially when iterating on UI.
https://github.com/TheBevyFlock/bevy_simple_subsecond_system
I think that’s what zig team is also doing to allow very fast build times: remove LLVM.
Yes, Zig author commented[0] that a while ago
[0] https://news.ycombinator.com/item?id=44390972
Nice, I checked a while ago and was no support for macOS aarch64, but seems that now it is supported.
Wait. You were going to ditch rust because of 16 second build times?
Over time that adds up when your coding consists of REPL like workflow.
Pulling out Instagram 100 times in every workday, yes, it's a total disaster
"Wait. You were going to ditch subversion for git because of 16 second branch merge times?"
Performance matters.
16 seconds is infuriating for something that needs to be manually tested like does this jump feel too floaty.
But it’s also probable that 16 seconds was fairly early in development and it would get much worse from there.
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 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.
Yeah, for application code in my experience the more I stick to the dumb way to do it the less I fight the borrow checker along with fewer trait issues.
Refactoring seems to take about the same time too so no loss on that front. After all is said and done I'm just left with various logic bugs to fix which is par for the course (at least for me) and a sense of wondering if I actually did everything properly.
I suppose maybe two years from now we'll have people that suggest avoiding generics and tempering macro usage. These days most people have heard the advice about not stressing over cloning and unwraping (though expect is much better imo) on the first pass more or less.
Something something shiny tool syndrome?
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.
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.
Many docker users develop on arm64-darwin and deploy to x86_64 (g)libc, so I don't think that'll work generally.
Those users are wrong :shrug:
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.
why rust compiler create so BIG executable!
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.
you had a functional and minimal deployment process (compile copy restart) and now you have...
...Kubernetes.
Damn, this makes such a great ad.
Just set up a build server and have your docker containers fetch prebuilt binaries from that?
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.
> 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.
One thing I like about Alpine Linux is how easy and dumbproof it is to make packages. It's not some wild beast like trying to create `.deb` files.
If anyone out there is already fully committed to using only Alpine Linux, I'd recommend trying creating native packages at least once.
I'm not familiar with .deb packages, but one thing I love about Arch Linux is PKGBUILD and makepkg. It is ridiculously easy to make a package.
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
What "most" cases are you thinking of? Also don't forget that a binary that in release weights 10 MB, when compiled with debug symbols can weight 300 MB, which is way less practical to distribute.
>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.
That's how Rust works already.
The problem has been created by Docker which destroys all of the state. If this was C, you'd also end up losing all of the object files and rebuilding them every time.
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.
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).
Rust can and does compile separate translation units in parallel; it's just that the translation unit is (roughly) a crate instead of a single C or C++ source file.
And even for crates, Rust has incremental compilation.
Is there an equivalent of ninja for rust yet?
It depends on what you mean by 'equivalent of ninja.'
Cargo is the standard build system for Rust projects, though some users use other ones. (And some build those on top of Cargo too.)
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.
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.
1 https://doc.rust-lang.org/reference/const_eval.html
Slow compile times are a feature, get to make a cuppa.
> Slow compile times are a feature
xkcd is always relevant: https://xkcd.com/303/
On the other hand you get mentally insane if you try to work in a way that you do s.th. usefull during the 5-10 min compile times you often have with C++ projects.
When I had to deal with this I would just open the newspaper and read an article in front of my boss.
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.
It would be more accurate to say that idiomatic Rust encourages doing things which are slow to compile: lots of small generic functions everywhere. And the most effective way to speed this up is to avoid monomorphization by using RTTI to provide a single generic compiled implementation that can be reused for different types, like what Swift does when generics across the module boundary. But this is less efficient at runtime because of all the runtime checks and computations that now need to be done to deal with objects of different sizes etc, many direct or even inlined calls now become virtual etc.
Here's a somewhat dated but still good overview of various approaches to generics in different languages including C++, Rust, Swift, and Zig and their tradeoffs: https://thume.ca/2019/07/14/a-tour-of-metaprogramming-models...
It's starting to, but a lot of people are using Rust because they need (or want) the best possible runtime performance, so that tends to be prioritised a lot of the time.
The ecosystem is vast, and different people have different priorities. Simple as that.
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.
C++ also has async these days.
As for templates, I can't think of anything about them that would speed up things substantially wrt Rust aside from extern template and manually managing your instantiations in separate .cpp files. Since otherwise it's fundamentally the same problem - recompiling the same code over and over again because it's parametrized with different types every time.
Indeed, out of the box I would actually expect C++ to do worse because a C++ header template has potentially different environment in every translation unit in which that header is included, so without precompiled headers the compiler pretty much has to assume the worst...
tldr as always, don't use Musl, if you want performance, compatibility.
tl;dr: it’s slow because it finds far more bugs before runtime than literally any other mainstream compiled language
[flagged]