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.
If anyone out there is already fully committed to using only Alpine Linux, I'd recommend trying creating native packages at least once.
Oops, changed one template in one header. And that impacts.... 98% of my code.
I don't know enough Rust, but I find these aspects are seriously lacking in C++ on Linux, and it is one of the few things I think Windows has it better for developers. Is Rust better?
Relevant: Subsecond: A runtime hotpatching engine for Rust hot-reloading - https://news.ycombinator.com/item?id=44369642 - June, 2024 (36 comments)
> Full IDE?
https://www.jetbrains.com/rust/ (newly free for non-commercial use)
> find these aspects are seriously lacking in C++ on Linux
https://www.jetbrains.com/clion/ (same, non-commercial)
A.k.a. "Remember the Vasa!" https://news.ycombinator.com/item?id=17172057
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
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.
(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.)
Respectfully, the parent only offers up a Zig compile time metric. That's it. That's the entire comment.
This HN post about Rust is now being dominated by a cheap shot Zig one liner humblebrag from the lead author of Zig.
I think this thread needs a little more nuance.
Being frustrated by perceived bad behavior doesn't mean responding with more bad behavior is a good way to improve the discourse, if that's your goal here.
That's not what I said. I said it's unlikely that fast compilation cannot be achieved while using LLVM which, I would argue, is proven by the existence of a fast compiler that uses LLVM.
All Zig code is built in a single compilation unit and everything is compiled from scratch every time you change something, including all dependencies and all the parts of the stdlib that you use in your project.
So you've been comparing Zig rebuilds that do all the work every time with Rust rebuilds that cache all dependencies.
Once incremental is fully released you will see instant rebuilds.
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.
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.
> Fil-C achieves this using a combination of concurrent garbage collection and invisible capabilities (each pointer in memory has a corresponding capability, not visible to the C address space)
With significant performance and memory overhead. That just isn't the same ballpark that Rust is playing in although hugely important if you want to bring forward performance insensitive C code into a more secure execution environment.
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?
> A brief note: 50 seconds is fine, actually!
50 seconds should actually not be considered fine.
Now we get all of this off-topic discussion about Zig. Which I guess is good for you Zig folk... But it's pretty off-putting for me.
whoisyc's comment is extremely on point. As the VP of community, I would really encourage thinking about what they said.
Having concrete proof that something can be done more efficiently is extremely important and, no, I haven't "demonstrated" anything, since my earlier comment would have had way less substance to it without the previous context.
The comment from Andrew is not just random compiler stats, but a datapoint showing a comparable example having dramatically different performance characteristics.
You can find in this very HN submission various comments that assume that Rust's compiler performance is impossible to improve because of reasons that actually are mostly (if not entirely) irrelevant. Case in point, see people talking about how Rust compilation must take longer because of the borrow checker (and other safety checks) and Steve pointing out that, no, actually that part of the compilation pipeline is very small.
> Now we get all of this off-topic discussion about Zig.
So no, I would argue the opposite: this discussion is very much on topic.
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.
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.
Cargo is the standard build system for Rust projects, though some users use other ones. (And some build those on top of Cargo too.)
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.
Do you really believe that nobody over the course of Rust's lifetime has ever taken a look at C compilers and thought about if techniques they use could apply to the Rust compiler?
Unity builds are useful for C programs because they tend to reduce header processing overhead, whereas Rust does not have the preprocessor or header files at all.
They also can help with reducing the number of object files (down to one from many), so that the linker has less work to do, this is already sort of done (though not to literally one) due to what I mentioned above.
In general, the conventional advice is to do the exact opposite: breaking large Rust projects into more, smaller compilation units can help do less "spurious" rebuilding, so smaller changes have less overall impact.
Basically, Rust's compile time issues lie elsewhere.
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 can believe that, but even so it's caused by the type system monomorphising everything. When it use qsort from libc, you are using per-compiled code from a library. When you use slice::sort(), you get custom assembler compiled to suit your application. Thus, there is a lot more code generation going on, and that is caused by the tradeoffs they've made with the type system.
Rusts approach give you all sorts of advantages, like fast code and strong compile time type checking. But it comes with warts too, like fat binaries, and a bug in slice::sort() can't be fixed by just shipping of the std dynamic library, because there is no such library. It's been recompiled, just for you.
FWIW, modern C++ (like boost) that places everything in templates in .h files suffers from the same problem. If Swift suffers from it too, I'd wager it's the same cause.
I was all excited to conduct the "cargo check; mrustc; cc" is 100x faster experiment, but I think at best, the multiple is going to be pretty small.
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.
This has tradeoffs: increased ABI stability at the cost of longer compile times.
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.
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.
Stuff like inserting bounds checking puts more work on the optimization passes and codegen backend as it simply has to deal with more instructions. And that then puts more symbols and larger sections in the input to the linker, slowing that down. Even if the frontend "proves" it's unnecessary that calculation isn't free. Many of those features are related to "safety" due to the goals of the language. I doubt the syntax itself really makes much of a difference as the parser isn't normally high on the profiled times either.
Generally it provides stricter checks that are normally punted to a linter tool in the c/c++ world - and nobody has accused clang-tidy of being fast :P
* 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).
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.
There's some other dependencies in there that are only used when building for test/benchmarking like serde, zstd, and criterion. You would need to be certain you're building only the library and not the test harness to be sure those aren't being built too.
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.
1. Use pointers and do not include header file for class, if you need pointer to that class. I think that's a pretty established pattern in C++. So if you want to declare pointer to a class in your header, you just write `class SomeClass;` instead of `#include "SomeClass.hpp"`.
2. Do not use STL or IOstreams. That project used only libc and POSIX API. I know that author really hated STL and considered it a huge mistake to be included to the standard language.
3. Avoid generic templates unless absolutely necessary. Templates force you to write your code in header file, so it'll be parsed multiple times for every include, compiled to multiple copies, etc. And even when you use templates, try to split the class to generic and non-generic part, so some code could be moved from header to source. Generally prefer run-time polymorphism to generic compile-time polymorphism.
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...
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.
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...
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.
But it’s also probable that 16 seconds was fairly early in development and it would get much worse from there.
https://github.com/TheBevyFlock/bevy_simple_subsecond_system
xkcd is always relevant: https://xkcd.com/303/
When I had to deal with this I would just open the newspaper and read an article in front of my boss.
> 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.
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.
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?
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.
> * 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...
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.
ahartmetz•6h ago
hu3•5h ago
> 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.
adastra22•5h ago
hu3•5h ago
First stage compiles the code. This is good for isolation and reproducibility.
Second stage is a lightweight container to run the compiled binary.
Why is the author being attacked (by multiple comments) for not making things simpler when that was not claimed that as the goal. They are modernizing it.
Containers are good practice for CI/CD anyway.
MobiusHorizons•5h ago
taberiand•2h ago
AndrewDucker•5h ago
Don't do what you don't need to do.
hu3•5h ago
They are already long past the point of "complicate things unnecessarily".
A simple Dockerfile pales in comparison.
adastra22•1h ago
dwattttt•4h ago
Docker is a (the, in some areas) modern way to do it, but far from the only way.
a3w•3h ago
eeZah7Ux•3h ago
no, that's sandboxing.
jerf•5h ago
"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.
scuff3d•3h ago
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.
linkage•1h ago
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.
vorgol•5h ago