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.
That is what they are doing. It's a 2 stage Dockerfile.
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.
Devcontainers are a good compromise though - you can develop within a context that can be very nearly identical to production; with a bit of finagling you could even use the same dockerfile for the devcontainer, and the build image and the deployed image
Because he spends a good deal of the intro complaining that this makes his dev practice slow. So don’t do it! It has nothing to do with docker but rather the fact he is wiping the cache on every triggered build.
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.
It's pretty clear that this whole project was god-tier level procrastination so I wouldn't worry too much about the details. The original stated problem could have been solved with a 5-line shell script.
Not strictly related, but I got to the parts about using a dependency to speed up builds in the container, and that his website has "hundreds" of Rust dependencies, and I was reminded why I get so annoyed with Rust. It's a great language, but the practice of just duct taping a bunch of dependencies together drives me nuts.
At work I prefer languages that take a "batteries included" approach. So Go and Python are good examples. You can get really far with just what is offered in the standard libraries. Though obviously you can still pull in a shitload of dependencies if you want.
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.
Nobody is suggesting to copy the binary to the Docker container.
When developing locally, use `cargo test` in your cli. When deploying to the server, build the Docker image on CI. If it takes 5 minutes to build it, so be 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.
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.
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.
How good is the debugger? "edit and continue"? Hot reload? Full IDE?
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?
"Rust devs don't use debuggers" isn't a good answer. The one time I used Rust for some project like 7 years ago, I did have to use a debugger, and it was fine.
If programmers don't use a debugger, that's because the debugger is bad.
For me, the ideal debugger is one you never leave. That is you are in a debugging session, maybe with your code running, or stopped on a breakpoint, and you write your code without leaving the session. You can see the values of your variables as you type them, the branches that will be taken, etc... When an error condition happen, you break on it, and still without leaving the session, you can fix the code, roll back before the error happened and see if it passes. IntelliJ is close to that, and it seems like some tools from the video game industry are even better.
Debugging should be natural, it shouldn't be something you pull out as a last resort. If it is not natural, it is a bad debugger, or a bad development environment, period. Maybe there are reasons for why it is bad, maybe the language designers have other priorities, which is acceptable, but it doesn't change the fact that it is bad. Same goes for slow compile times by the way.
I've only ever really used a debugger on embedded, we used gdb there. I know VS: Code has a debugger that works, I'm sure other IDEs do too.
> edit and continue
Hard to do in a pre-compiled language with no runtime, if you're asking about what I think you're asking about.
> Hot reload
Other folks gave you good links, but this stuff is pretty new, so I wouldn't claim that this is great and often good and such.
> Full IDE
I'm not aware of Rust-specific IDEs, but many IDEs have good support for Rust. VS: Code is the most popular amongst users according to the annual survey. The Rust Project distributes an official LSP server, so you can use that with any editor that supports it.
I haven't used Rust much, but the tooling felt very solid. There's a default package manager that works well, unlike many other languages including C++ and somehow Python. Debugging is fine. Idk why you expected edit-and-continue, it's not like you get that in C++ either.
You have "edit and continue" in Visual Studio (the real IDE, not VS Code).
And I mentioned it as a downside of C++ on Linux, and I would expect a language that has "the best" tooling to have that.
C++ tooling isn't that great, but it has one thing going for it: it is popular in the video game industry, and the video industry has some of the best tools.
And sure enough, if by tooling you mean "package management", I'd say everything is better than C++, and on the other side, it seems that cargo is pretty good. I don't know how they tackle the "left-pad" problem that plagues npm though. By that I mean supply-chain attacks.
It's not like npm is particularly bad at handling supply-chain attacks, it's just a very popular ecosystem and gets targeted more as a result. Idk how you truly solve this without code audits, and if anything the more popular/visible packages will be audited more.
Btw, left-pad fallout wasn't all that bad. It's not like the author put something malicious into the code. For less than a day, people couldn't download that dep from npm. If someone really needed to fix a build, they could copy in a backup. Pretty sure a typical C++ or Python project build gets broken on its own more often than that.
> Idk how you truly solve this without code audits
Idk either, but code audits are definitely a solution. Take Debian packages for instance. Debian has package maintainers, and while they may no do full audits, they will at least test it before publishing. In addition, it doesn't get in the "stable" release before an extensive testing phase. Security patches are usually backported.
Or do like with the Apple App Store, where you don't get to publish anything unreviewed.
These are not perfect solution, there is no such thing as a perfect solution. For instance, Debian is famously lagging behind in versions, and the App Store will sometimes reject your app for no good reason, while being expensive. In every case there is some barrier to entry, a slow process, and it costs time and money, but that mitigates some of the issues.
Npm seems to have very little safeguards, has a culture of always taking the latest version, and as a result is often victim to supply-chain attacks. I don't think it is just popularity. Debian is really popular too, but AFAIK, it doesn't have this problem, in fact, one of the best known supply-chain attack is the xz library, and Debian didn't fall to it.
Slow compared to what? I’m still scraping my head at this. My cargo builds are insanely fast, never taking more than a minute or two even on large projects. The only ahead of time compiled language I’ve used with faster compilation speed is Go, and that is a language specifically designed around (and arguably crippled by) the requirement for fast compilation. Rust is comparable to C compilation, and definitely faster than C++, Haskell, Java, Fortran, Algol, and Common Lisp.
Just a few days ago I used cargo to install something. It took like two minutes at the last stage. Definitely not comparable to C or Fortran. I never had to wait that much before. With C++? Definitely. Never with C though.
In addition to Go, off the top of my head, you have D, V-lang, Zig, Lua and every interpreted language (including Haskell and Common Lisp, as they both have interpreters). Traditional compiled languages always had terrible UX in this regard which, IMO, is why interpreted languages like Python and Ruby got so popular.
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.
There's a lot of overlap, but not that simple. Unless you also discount C issues that C++ inherits. Even then, there's subtleties and differences between the two that matter.
Not sure about you, but traditionally my C++ have always been faster than out of the box experience with Rust.
Thanks to pre-compiled headers, incremental compiler, incremental linker, and using binary libraries for all dependencies. Nowadays modules as well.
Rust compiles from scratch, some crates might get compiled multiple times due to conflicting configurations on the dependency graph, binary libraries require additional setup for something like scache.
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 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
The memory usage is quite large compared to C/C++ when compiling. I use Virtual Machines for Demos on my YouTube Channel and compiling something large in Rust requires 8GB+.
Maybe something else is going on then. I've done builds of some large open source projects and most of the time they was maxing the cores (I was building j32) but memory usage was fine.
The one that immediately comes to mind is cvc5... not super recently though.
I suspect that "tried to" is doing a bit of work here. The fact that it was failing and swapping out probably meant that the more memory heavy g++ processes were going slower than the memory light ones, resulting in more of them running simultaneously than would likely have happened in a normal successful build. Still, this was on a system with 32GB of ram, so it was using roughly that before swapping would slow down more memory intensive processes.
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.
(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.)
FWIW, I think your revised comment is far better, even though I disagree with some of the framing, there's at least some substance there.
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.
It's all good. I'm very guilty of bad behavior myself a lot of the time. It's on all of us to give gentle nudges when we see each other getting out of line. I deserve to be told the same if you see me doing this too!
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.
@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?
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
LLVM isnt a good scapegoat. A C application equivalent in size to a rust or c++ application will compile an order of magnitude quicker and they all use LLVM. I'm not a compiler expert, but it doesn't seem right to me that the only possible path to quick compilation for Zig was a custom backend.
Be that as it may, many C compilers are still an order of magnitude faster than LLVM. Probably the best example is tcc, although it is not the only one. C is a much simpler language than rust, so it is expected that compilation should take less time for C. That doesn’t mean llvm isn’t a significant contributor to compilation speed. I believe cranelift compilation of rust is also much faster than the llvm path
> That doesn’t mean llvm isn’t a significant contributor to compilation speed.
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.
It will compile an order of magnitude quicker because it often doesn't do the same thing - e.g. functions that are aggressively inlined in C++ or Rust or Zig would be compiled separately and linked normally, and generally there's less equivalent of compile-time generics in C code (because you have to either spell out all the instantiations by hand or use preprocessor or a code generator to do something that is two lines of code in C++).
Honestly I think it's good to highlight it. As a industry we're too hampered by "Don't even try that, use the existing thing" and it's causing these end results.
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.
Zig is starting to migrate to custom backends for debug builds (instead of using LLVM) plus incremental compilation.
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.
When targeting x86_64, the self-hosted backend is already enabled by default on the latest builds of Zig (when compiling in Debug mode). The self-hosted aarch64 backend currently isn't generally usable (so we still default to LLVM when targeting aarch64), but it's likely to be the next ISA we focus on codegen for.
Anyhoodle, I'm looking forward to testing the compile times again when Zig says the build times are fast now.
But if we're talking about incrementality, I look forward to testing that too when it's ready. I've got a Bazel build for work that pulls together Rust along with a custom (very slow) compiler and the cached successful pipelines take 2-3 minutes (mostly the benchmark run) and the ones that churned the non-rust take 15-18 minutes, of which about 10-11 minutes is just compiling the non-rust.
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.
> Rust has made several language design decisions that make compiler performance difficult
Do you have a list off the top of your head/do you know of a decent list? I've now read many "compiler slow" thoughtpieces by many people and I have yet to see someone point at a specific feature and say "this is just intrinsically harder".
I believe that it likely exists, but would be good to know what feature to get mad at! Half joking of course
You can have your house built fast, cheap, or well. Pick two; or a bit of all three that adds up to the same effort required. You can't have all three.
You can't have a language with 100% of the possible runtime perf, 100% of the possible compile speed and 100% of the possible programmer ease-of-use.
At best you can abuse the law of diminishing returns aka the 80-20 rule, but that's not easy to balance and you run the risk of creating a language that's okay at everything, but without any strong selling points, like the stellar runtime performance Rust is known for.
So a better way to think about it is: Given Rust's numerous benefits, is having subpar compilation time really that big of a deal?
> Given Rust's numerous benefits, is having subpar compilation time really that big of a deal?
As someone who uses Rust as a daily driver at work at zed.dev (about 600K LoC of Rust), and Zig outside of work on roc-lang.org (which was about 300K LoC of Rust before we decided to rewrite it in Zig, in significant part because of Rust's compilation speed), yes - it is an absolutely huge deal.
I like a lot of things about Rust, but its build times are my biggest pain point.
I think this qualitative argument is made but is very unsatisfying for something as quantitative as compilation or runtime speeds.
It leads to blithe things about how there's no such things as zero-cost abstractions. But at one point the cost is so low and amortized that you're looking at something that's basically free.
What is the compilation cost of the `?` syntax shorthand? Probably quite low!
What about the compilation cost of underscores in number literals? Again, quite low?
To this point a bit, I think a lot of people talk about the borrow checker, but my understanding is it's fairly low cost, and I don't worry about it.
Monomorphisation costs though? That feels like something that could generate a lot of work.
Rust heavily uses value types with specialized generics, which explodes the work needed by the compiler. It can - sometimes - improve performance. But it always slows down compilation.
Let's dig into this bit of that, to give you some more color:
> 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.
Rust decided to go with the classic separate compilation model that languages like C use. Let's talk about that compared to Zig, since it was already brought up in this thread.
So imagine we have a project, A, and it depends on B. B is a huge library, 200,000 lines of code, but we only use one function from it in A, and that function is ten lines. Yes, this is probably a bad project management decision, but we're using extremes here to make a point.
Cargo will compile B first, and then A, and then link things together. That's the classic model. And it works. But it's slow: rust had to compile all 200,000 lines of code in B, even though we only are gonna need ten lines. We do all of this work, and then we throw it away at the end. A ton of wasted time and effort. This is often mitigated by the fact that you compile B once, and then compile A a lot, but this still puts a lot of pressure on the linker, and generics also makes this more complex, but I'm getting a bit astray of the main point here, so I'll leave that alone for now.
Zig, on the other hand, does not do this. It requires that you compile your whole program all at once. This means that they can drive the compilation process beginning from main, in other words, only compile the code that's actually reachable in your program. This means that in the equivalent situation, Zig only compiles those ten lines from B, and never bothers with the rest. That's just always going to be faster.
Of course, there are pros and cons to both of these decisions, Rust made the choice it did here for good reasons. But it does mean it's just going to be slower.
I do wonder if the advantages of the separate compilation model are being realized. Like I can understand having the semantics of a separate compilation model (and I think you could keep those without actually implementing with them?) but if the compiler really did just pull in what it needed and nothing more something tells me we could see massive perf improvements from dependencies.
I suppose a lot of the coherence checking still "needs" to happen, but how I would love a "--i-dont-care-about-coherence-in-dependent-traits" flag or something. I know I'm missing a lot
One difference that Zig has is that it doesn't have multiline comments or multiline strings, meaning that the parser can parse any line correctly without context. I assume this makes parallelization trivial.
There is ino operator overloading like C, so A + B can only mean one thing.
You can't redeclare a variable, so foo can only map to one thing.
The list goes on.
Basically it was designed to compile faster, and that means many issues on Github have been getting rejected in order to keep it that way. It's full of compromises.
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.
> 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.
Fil-C has advanced a lot since I last looked at it:
> Fil-C is currently 1.5x slower than normal C in good cases, and about 4x slower in the worst cases.
with room for optimization still. Compatibility has improved massively too, due to big changes to how it works. The early versions were kind of toys, but if Filip's claims about the current version hold up then this is starting to look like a very useful bit of kit. And he has the kind of background that means we should take this seriously. There's a LOT of use cases for taking stuff written in C and eliminating memory safety issues for only a 50% slowdown.
> 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.
Your first claim is unverifiable and the second one is just so, so wrong. Even big projects with very talented, well-paid C or C++ devs eventually end up with CVEs, ~80% of them memory-related. Humans are just not capable of 0% error rate in their code.
If Zig somehow got more popular than C/C++, we would still be stuck in the same CVE bog because of memory unsafety. No thank you.
> If Zig somehow got more popular than C/C++, we would still be stuck in the same CVE bog because of memory unsafety. No thank you.
Zig does a lot of things to prevent or detect memory safety related bugs. I personally haven't encountered a single one so far, while learning the language.
> ~80% of them memory-related.
I assume you're referencing the 70% that MS has published? I think they categorized null pointer exceptions as memory safety bugs as well among other things. Zig is strict about those, has error unions, and is strict and explicit around casting. It can also detect memory leaks and use after free among other things. It's a language that's very explicit about a lot of things, such as control flow, allocation strategies etc. And there's comptime, which is a very potent tool to guarantee all sorts of things that go well beyond memory safety.
I almost want to say that your comment presents a false dichotomy in terms of the safety concern, but I'm not an expert in either Rust or Zig. I think however it's a bit broad and unfair.
What? Zig is definitively not memory-safe, while safe Rust, is, by definition, memory-safe. Unsafe rust is not memory-safe, but you generally don't need to have a lot of it around.
It’s a ten year old bug because it has never been found in the wild, ever, in those ten years. Low impact, high implementation effort bugs take less priority than bugs that affect real users.
Ferrocene is intended to document the behavior of the current version of the rustc compiler, so it's just an effort to formalize "the compiler is the language".
Yes, the soundness hole itself is low impact and doesn't need to be prioritized but it undermines the binary "Zig is definitively not memory-safe, while safe Rust, is, by definition, memory-safe" argument that was made in response to me. Now you're dealing with qualitative / quantitative questions of practical impact, in which my original statement holds: "Zig is less memory safe than Rust, but more than C/C++. Neither Zig nor Rust is fundamentally memory safe."
You can of course declare that Safe Rust is by definition memory safe, but that doesn't make it any more true than declaring that Rust solves the halting problem or that it proves P=NP. RustBelt is proven sound. Rust by contrast, as being documented by Ferrocene, is currently fundamentally unsound (though you won't hit the soundness issues in practice).
I believe these two statements should show the fundamental difference:
- If a safe Rust program exhibits a memory safety problem, it is a bug in the Rust compiler that is to be fixed
- If a Zig program exhibits a memory safety problem, it is a bug in the Zig program that needs to be fixed, not in the compiler
Wouldn't you agree?
> Ferrocene is intended to document the behavior of the current version of the rustc compiler, so it's just an effort to formalize "the compiler is the language".
I must admit I haven't read the specification, but I doubt they attempt to be "bug for bug" compatible in the sense that the spec enumerates memory safety bugs present in the Rust compiler. But am I then mistaken?
No, I don't agree. A compiler bug is something that gets fixed in a patch release after it's reported, or perhaps some platform-specific regression that gets fixed in the next release after it's reported. What we're discussing by contrast is a soundness hole in the language itself - one which will most likely require breaking changes to the language to close (i.e. some older programs that were perfectly safe will fail to compile as a side effect of tightening up the Rust language to prevent this soundness hole).
As to the Ferrocene specification, it explicitly states "Any difference between the FLS and the behavior of the Rust compiler is considered an error on our part and the FLS will be updated accordingly."
Proposals to fix the soundness hole in Rust either change the variance rules themselves, or require where clauses in certain places. Either of these changes would require corresponding changes to chapter 4 of the Ferrocene specification.
And Rust has and will make those breaking changes, while Zig will likely not. In fact there are documented and blessed ways to break memory safety in Zig, and no one is calling them soundness bugs!
I really don’t see how you can claim with a straight face that the two approaches are the same.
"In fact there are documented and blessed ways to break memory safety in Zig" - just as there are in Rust... even the Rust standard library makes liberal use of them (thereby making any program which invokes those parts of the standard library transitively unsafe by definition).
Look, I'm not saying the Zig and Rust approaches are the same. I explicitly stated that Rust is more memory safe than Zig (which is in turn more memory safe than C/C++).
This is because Rust has clearly delineated a "safe" subset of Rust which you have to explicitly opt out of that is mostly sound (and has a goal of eventually being entirely sound), has a culture of encouraging the use of the safe subset, and has taken a good approach to the interfacing of safe and unsafe code (i.e. if unsafe code is properly written and satisfies the exposed contract - despite the compiler being unable to verify this - then safe code can safely be linked with it).
All of this results in extremely low risk of memory corruption for Rust programs in practice (far lower than any other commonly used non-GC language with the sole exception of SPARK).
What you can't do though is reject the notion of memory safety being a sliding scale and draw a binary distinction between languages that are 100% perfectly memory safe and languages that are memory unsafe. Well you can, but Rust will fall on the side of memory unsafe for many years to come. Java (ignoring vendor-specific extensions) falls on the safe side though - the language semantics as specified are sound and it doesn't even have an unsafe subset.
On this sliding scale you insist on, I think most would agree with if we’re insisting on being pedantic which isn’t a great place to have informal discussion. Regardless, correct me if I’m wrong - on the sliding scale Zig isn’t actually that far away from C/C++ whereas Rust is more like C#/JS/Java etc. it also broadens the definition of memory safety to include race conditions whereas Zig is still like C/C++ there (and indeed I can’t think of another language that provides the safety against races that Rust does). Moreover it does this without sacrificing any performance.
Look, Zig has neat ideas. No disputing that. But on the sliding scale there’s a threshold where people are comfortable classifying something as being memory safe to avoid perfect being the enemy of good. And by that classification criteria, Zig falls well below the bar and Rust clears it easily.
> As to the Ferrocene specification, it explicitly states "Any difference between the FLS and the behavior of the Rust compiler is considered an error on our part and the FLS will be updated accordingly."
Right, this is from before it's adopted as the actual spec, because it was from outside the project, and so could not be.
Also, these goalposts are moving: it was "Rust doesn't have a spec" and now it's "I don't like the spec."
Fixing this soundness hole does not require a breaking change to the language. It is an implementation bug, not a problem with the language as specified. But even if it were, Rust's policies around soundness do allow for this, and the project has done it in the past.
The goalposts haven't moved. The goalposts were always "the current compiler is the language".
If there is a proposed fix to the soundness hole that wouldn't reject some existing sound Rust code, please link to it; none of the proposed fixes I've seen do so. And yes, Rust's policies do allow for breaking changes in pursuit of soundness - likely some day many years from now safe Rust will indeed be sound and guaranteed to be memory safe.
> If a safe Rust program exhibits a memory safety problem, it is a bug in the Rust compiler that is to be fixed - If a Zig program exhibits a memory safety problem, it is a bug in the Zig program that needs to be fixed, not in the compiler
That is the absolute best description of memory safety I’ve heard expressed.
By that definition, Python is not memory-safe, Java is not memory-safe, Go is not memory-safe, and so on. All of these languages contain escape hatches to do memory-unsafe stuff, yet no one is calling them memory unsafe.
Go is more memory unsafe than Java or Rust. Data races in concurrent Go code can cause memory corruption, unlike in concurrent Java code. Safe Rust is designed to avoid data races altogether using static analysis.
"Over 50%" only holds if the statement is intended to be binary. It may be that he considers direct use, no use, and transitive use to be all different. In which case it is possible that the majority[1] use unsafe, even if more than 50% does not.
[1] The cult of the orange man would call this a plurality, which may be what has tripped you up, but the civilized world calls it a majority.
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?
As you've just demonstrated, that point can be made without even mentioning Zig, let alone copy/pasting some compile time stuff with no other comment or context. Which is why I thought (well, hoped) there might be something more to it than just a dunk attempt.
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.
> As you've just demonstrated, that point can be made without even mentioning Zig, let alone copy/pasting some compile time stuff with no other comment or context. Which is why I thought (well, hoped) there might be something more to it than just a dunk attempt.
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.
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).
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.
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.
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.
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.
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?".
> "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?".
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?
Rust and C have differences around compilation units: Rust's already tend to be much larger than C on average, because the entire crate (aka tree of modules) is the compilation unit in Rust, as opposed to the file-based (okay not if you're on some weird architecture) compilation unit of C.
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.
Can you explain why a unity build would help? Conventional wisdom is that Rust compilation is slow in part because it has too few translation units (one per crate, plus codegen units which only sometimes work), not too many.
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.
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.
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.
> Most of the build time is spent in the code generation phase.
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.
It's partly by the type system. You can implement a std::sort (or slice::sort()) that just delegates to qsort or a qsort-like implementation and have roughly the same compile time performance as just using qsort straight.
But not having to is a win, as the monomorphised sorts are just much faster at runtime than having to do an indirect call for each comparison.
This is a pattern a crate author can rely on (write a function that uses genetics that immediately delegates to a function that uses trait objects or converts to the needed types eagerly so the common logic gets compiled only once), and there have been multiple efforts to have the compiler do that automatically. It has been called polymorphization and it comes up every now and then: https://internals.rust-lang.org/t/add-back-polymorphization/...
All true, but given the number of "Rust compile time is slow" posts that blame the compiler I think it's safe to say most programmers don't understand the real underlying trade-off that's causes it.
Not all programmers of course - if you look at std there are many places that split types into generic and non-generic parts so the compiler will reuse as much code as possible, but it does come at the cost of additional complexity. Worse if you aren't already aware of why they are doing it, the language does a marvellous job of hiding the reason that complexity is there. I'd wager a novice Rust programmer is as befuddled by it as a JavaScript programmer coming across his first free() call in C.
I have this dream of a language like Rust that makes the trade-off plain, so the programmer is always aware of "this is a zero cost abstraction - you're just making it plain via the type system your doing the right thing" and "I'm going to have to generate a lot of code for this". Then go a step further and put the types and source you want to export to other libraries in a special elf section in the .so so you don't need the source to link against it, then go another step further and make the programmer using the .so explicitly instantiate any stuff that does require a lot of code generated so he is aware of what is happening.
That said, I don't think it would help the compile time problem in most cases. C++ already does something close by forcing you to put exported stuff in .h files, and they ended up with huge .h files and slow compiles anyway.
Nevertheless doing that would make for a Rust like language, that, unlike Rust, supported an eco-system of precompiled libraries just like C does. Rust is so wedded to transparent monomorphisation it looks near impossible now.
I just ran cargo check on nushell, and it took a minute and a half. I didn't time how long it took to compile, maybe five minutes earlier today? So I would call it faster, but still not fast.
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.
Did you do it from a clean build? In that case, it's actually a slightly misleading metric, since rust needs to actually compile macros in order to typecheck code that uses them. (And therefore must also compile all the code that the macro depends on.) My bad for suggesting it, haha. Incremental cargo check is often a better way of seeing how long typechecking takes, since usually you haven't modified any macros that will need to be recompiled. On my project at work, incremental cargo check takes `1.71s`.
Side note: There's an effort to cache proc macro invocations so that they get executed only once if the item they annotate hasn't changed: https://github.com/rust-lang/rust/pull/129102
There are multiple caveats on providing this to users (we can't assume that macro invocations are idempotent, so the new behavior would have to be opt in, and this only benefits incremental compilation), but it's in our radar.
The actual reason that you don't have to care about this on your C++ project is because C++ doesn't let you define macros in C++, you can only define them in the preprocessor language. Therefore no compilation is needed to execute them.
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.
Not only does it generate more code, the initially generated code before optimizations is also often worse. For example, heavy use of iterators means a ton of generics being instantiated and a ton of call code for setting up and tearing down call frames. This gets heavily inlined and flattened out, so in the end it's extremely well-optimized, but it's a lot of work for the compiler. Writing it all out classically with for loops and ifs is possible, but it's harder to read.
The swift compiler is definitely bottle necked by type checking. For example, as a language requirement, generic types are left more or less in-tact after compilation. They are type checked independent of what is happening. This is unlike C++ templates which are effectively copy-pasting the resolved type with the generic for every occurrence of type resolution.
This has tradeoffs: increased ABI stability at the cost of longer compile times.
A lot can be done by the programmer to mitigate slow builds in Swift. Breaking up long expressions into smaller ones and using explicit types where type inference is expensive for example.
I’d like to see tooling for this to pinpoint bottlenecks - it’s not always obvious what’s making builds slow.
> This has tradeoffs: increased ABI stability at the cost of longer compile times.
Nah. Slow type checking in Swift is primarily caused by the fact that functions and operators can be overloaded on type.
Separately-compiled generics don't introduce any algorithmic complexity and are actually good for compile time, because you don't have to re-type check every template expansion more than once.
Modern C++ in particular does a lot of similar, albeit not identical, codegen due to its extensive metaprogramming facilities. (C is, of course, dead simple.) I've never looked into it too much but anecdotally Rust does seem to generate significantly more code than C++ in cases where I would intuitively expect the codegen to be similar. For whatever reason, the "in theory" doesn't translate to "in practice" reliably.
I suspect this leaks into both compile-time and run-time costs.
> Once you add static linking to the toolchain (in all of its forms) things get really fucking slow.
Could you expand on that, please? Every time you run dynmically linked program, it is linked at runtime. (unless it explicitly avoids linking unneccessary stuff by dlopening things lazily; which pretty much never happens). If it is fine to link on every program launch, linking at build time should not be a problem at all.
If you want to have link time optimization, that's another story. But you absolutely don't have to do that if you care about build speed.
Reading your comment is sounds like the opposite would be true, because so much linking would be needed to be done at runtime. But that perception fails to realize, that when claiming an executable is linked dynamically, most symbols were also statically linked. It is only the few public exported symbols that are dynamically linked, because there are deemed to be a reasonable separate concern, that should be handled by someone elses codebase.
I think lazily linking is the default even if you don't use dlopen, i.e. every symbol gets linked upon first use. Of course that has the drawback, that the program can crash due to missing/incompatible libraries in the middle of work.
A lot of vendors use non-lazy binding for security reasons, and some platforms don't support anything other than RTLD_NOW (e.g., Android).
Anyway, while what you said is theoretically half-true, a fairly large number of libraries are not designed/encapsulated well. This means almost all of their symbols are exported dynamically, so, the idea that there are only "few public exported symbols" is unfortunately false.
However, something almost no one ever mentions is that ELF was actually designed to allow dynamic libraries to be fairly performant. It isn't something I would recommend, as it breaks many assumptions on Unices, (while you don't get the benefits of LTO) you can achieve code generation almost equivalent to static linking by using something like "-fno-semantic-interposition -Wl,-Bsymbolic,-z,now". MaskRay has a good explanation on it:
https://maskray.me/blog/2021-05-16-elf-interposition-and-bsy...
Go got famous compile times, because for a decade a new generation educated in scripting languages and used to badly configured C and C++ projects, took for innovation, what was actually a return to old values in compiler development.
The meme that static linking is slow or produces anything other than the best executables is demonstrably false and the result of surprisingly sinister agendas. Get out readelf and nm and PS sometime and do the arithematic: most programs don't link much of glibc (and its static link is broken by design, musl is better at just about everything). Matt Godbolt has a great talk about how dynamic linking actually works that should give anyone pause.
DLLs got their start when early windowing systems didn't quite fit on the workstations of the era in the late 80s / early 90s.
In about 4 minutes both Microsoft and GNU were like, "let me get this straight, it will never work on another system and I can silently change it whenever I want?" Debian went along because it gives distro maintainers degrees of freedom they like and don't bear the costs of.
Fast forward 30 years and Docker is too profitable a problem to fix by the simple expedient of calling a stable kernel ABI on anything, and don't even get me started on how penetrated everything but libressl and libsodium are. Protip: TLS is popular with the establishment because even Wireshark requires special settings and privileges for a user to see their own traffic, security patches my ass. eBPF is easier.
Dynamic linking moves control from users to vendors and governments at ruinous cost in performance, props up bloated industries like the cloud compute and Docker industrial complex, and should die in a fire.
Don't take my word for it, swing by cat-v.org sometimes and see what the authors of Unix have to say about it.
I'll save the rant about how rustc somehow manages to be slower than clang++ and clang-tidy combined for another day.
I didn't say anything was a conspiracy, let alone everything. I said inferior software is promoted by vendors on Linux as well as on MacOS and Windows with unpleasant consequences for users in a way that serves those vendors and the even more powerful institutions to which they are beholden. Sinister intentions are everywhere in this business (go read the opinions of the people who run YC), that's not even remotely controversial.
If fact, if there was anything remotely controversial about a bunch of extremely specific, extremely falsifiable claims I made, one imagines your rebuttal would have mentioned at least one.
I said inflmatory things (Docker is both arsonist and fireman at ruinous cost), but they're fucking true. That Alpine in the Docker jank? Links musl!
You said something false and important and I took the opportunity to educate anyone reading about why this aspect of their computing experience is a mess. All of that is germane to how we ended up in a situation where someone is calling rustc with a Dockerfile and this is considered normal.
No one is trying to take anyone's multi-gigabyte pile of dynamic library closure to deploy what should be a few hundred kilobytes of arbitrarily portable, secure by construction, built to last executable.
But people should make an informed choice, and there isn't any noble or high minded or well-meaning reason to try to shout that information down.
Don't confidently assert falsehoods unless you're prepared to have them refuted. You're entitled to peddle memes and I'm entitled to reply with corrections.
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.
Is gccgo really faster? Last time I looked it looked like it was abandoned (stuck at go 1.18, had no generics support) and was not really faster than the "actual" compiler.
Digging around, looks like it's workload dependent.
For pure computational workloads, it'll be faster. However, anything with heavy allocation will suffer as apparently the gccgo GC and GC related optimizations aren't as good as cgo's.
Go defaults to an unoptimized build. If you want it to run heavy optimization passes, you can turn those on with flags. Rust defaults to doing most of those optimizations on every build and allows you to turn them off.
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.
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.
C++23 modules compile really fast now, moreso when helped with incremental compilation, incremental linking, and a culture that has not any qualms depending on binary libraries.
Yeah, I deal with multiple Go projects that take a couple minutes to link the final binary, much less build all the intermediates.
Compilation speed depends on what you do with a language. "Fast" is not an absolute, and for most people it depends heavily on community habits. Rust habits tend to favor extreme optimizability and/or extreme compile-time guarantees, and that's obviously going to be slower than simpler code.
Try https://github.com/ncruces/go-sqlite3 it runs sqlite in WASM with wazero, a pure Go WASM runtime, so it builds without any CGo required. Most of the benchmarks are within a few % of the performance of mattn/go-sqlite3.
Thats not really true. As a counter example, Ocaml has a very advanced type system, full typeinference, generics and all that jazz. Still its on par, or even faster to compile than Go.
>When you have things like macros, advanced type systems, and want robustness guarantees at build time.. then you have to pay for that.
Go and Dlang compilers were designed by those that are really good at compiler design and that's why they're freaking fast. They designed the language around the compiler constraints and at the same managed to make the language intuitive to use. For examples, Dlang has no macro and no unnecessary symbols look-up for the ambiguous >>.
Because of these design decisions both Go and Dlang are anomaly for fast compilation. Dlang in particular is notably more powerful and expressive compared to C++ and Rust even with its unique hybrid GC and non-GC compilation.
In automotive industry it's considered a breakthrough and game changing achievement if you have a fast transmission for seamless auto and manual transmission such as found in the latest Koenigsegg hypercar [1]. In programming industry however, nobody seems to care. Walter Bright the designer of Dlang has background in mechanical engineering and it shows.
[1] Engage Shift System: Koenigsegg new hybrid manual and automatic gearbox in CC850:
It isn't an anomaly, it was pretty standard during the 1990's, until C and C++ took over all other compiled languages, followed by a whole generation educated in scripting languages.
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.
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.
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.
While the borrow checker is one big difference, it's certainly not the only thing the rust compiler offers on top of C that takes more work.
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
It truly is not about bounds checks. Index lookups are rare in practical Rust code, and the amount of code generated from them is miniscule.
But it _is_ about the sheer volume of stuff passed to LLVM, as you say, which comes from a couple of places, mostly related to monomorphization (generics), but also many calls to tiny inlined functions. Incidentally, this is also what makes many "modern" C++ projects slow to compile.
In my experience, similarly sized Rust and C++ projects seem to see similar compilation times. Sometimes C++ wins due to better parallelization (translation units in Rust are crates, not source files).
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.
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.
Probably, but it's the kind of thing that needs a lot of fairly significant overhauls in the compiler architecture to really move the needle on, as far as I understand.
You can address the junk problem manually by having generic functions delegate as much of their work as possible to non-generic or "less" generic functions (Where a "less" generic function is one that depends only on a known subset of type traits, such as size or alignment. Then delegating can help the compiler generate fewer redundant copies of your code, even if it can't avoid code monomorphization altogether.)
I believe the specific advice they're referring to has been stable for a while. You take your generic function & split it into a thin generic wrapper, and a non-generic worker.
As an example, say your function takes anything that can be turned into a String. You'd write a generic wrapper that does the ToString step, then change the existing function to just take a String. That way when your function is called, only the thin outer function is monomorphised, and the bulk of the work is a single implementation.
It's not _that_ commonly known, as it only becomes a problem for a library that becomes popular.
Well, zero-cost abstractions are still abstractions. It’s not junk per-se, but things that will be optimized out if the IR has enough information to safely do so, so basically lots of extra metadata to actually prove to LLVM that these things are safe.
> 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.
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.
Again, as this been often repeated, and backed up with data, the borrow-checker is a tiny fraction of a Rust apps build time, the biggest chunk of time is spent in LLVM.
The borrow checker is really not that expensive. On a random example, a release build of the regex crate, I see <1% of time spent in borrowck. >80% is spent in codegen and LLVM.
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.
The 'essentially no dependencies' isn't entirely true. It depends on the 'windows' crate, which is Microsoft's auto-generated Win32 bindings. The 'windows' crate is huge, and would be leading to hundreds of thousands of LoC being pulled in.
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.
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 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.
Templates as one single feature can be hugely variable. Its effect on compilation time can be unmeasurable. Or you can easily write a few dozen lines that will take hours to compile.
So there are few distinctive patterns I observed in that project. Please note that many of those patterns are considered anti-patterns by many people, so I don't necessarily suggest to use them.
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.
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.
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?
How many LOC is unreal? I'm trying to estimate whether making LLVM compatible with UNITY_BUILD would be worth the effort.
EDIT: i signed up to get access to unreal so take a look at how they do unity builds and turns out they have their own build tool (not CMake) that orchestrates the build. so does anyone know (can someone comment) whether unity builds for them (unreal) means literally one file for literally all project sources files or if it's "higher-granularity" like UNITY_BUILD in CMake (i.e., single file per object).
> If you want to see the difference download unreal engine and compile the editor with and without unity builds enabled.
UE doesn't use a full unity build, it groups some files together into small "modules". I can see how this approach may have some benefits; you're trading off a faster clean build for a slower incremental build.
I tested compiling UnrealFrontend, and a default setup with the hybrid unity build took 148s. I noticed it was only using half my cores due to memory constraints. I disabled unity and upped the parallelism and got 182s, so 22% slower while still using less memory. A similarly configured unity build was 108s, so best case is ~2x.
On the other hand only changing the file TraceTools/SFilterPreset.cpp resulted in 10s compilation time under a unity build, and only 2s without unit.
I can see how this approach has its benefits (and drawbacks). But to be clear this isn't what projects like raddbg and sqlite3 are doing. They're doing a single translation unit for the entire project. No parallelism, no incremental builds, just a single compiler invocation. This is usually what I've seen people mean by a unity build.
> 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?
I just did a release build of egui in 35s, about the same as raddbg's release build. This includes compiling dependencies like wgpu, serde and about 290 other dependencies which add up to well over a million lines of code.
Note I do have mold configured as my linker, which speeds things up significantly.
I think the definition of unity build is sufficiently unclear that it can be used as a pro or a con. It’s one of the problems with c++ - there’s no default build tool to compare with. II’d argue that a single unity blob with one compiler invocation isn’t a fair comparison and that when you made a token effort to configure the unity build you saw a 2x speed up.
I’m the tech director for a game studio using unreal and we spec the dev machines with enough memory to avoid that pressure - we require 2.5GB/Core, rounded up. Right now we’re using i9 14900k’s with 128GB RAM. We were on 5950x with 64GB before this.
I tried egui last night but failed to build it due to missing dependencies, but I’ll see if I can get it running on Monday. I’d also love to see what raddbg would be like if we split it into even 4 compilation units
> II’d argue that a single unity blob with one compiler invocation isn’t a fair comparison and that when you made a token effort to configure the unity build you saw a 2x speed up.
Most certainly an unfair comparison; I did not know that "unity build" also referred to a hybrid setup. I've only seen people advocate for a single translation unit approach, and that's always just plain slow.
Personally I'll almost always prefer faster incremental compilation; thankfully my work's clean builds only take 30 seconds.
> I’d also love to see what raddbg would be like if we split it into even 4 compilation units
Should be a minimum of 32x faster just building in parallel with our hardware ;)
Unfortunately raddbg suffers from unity-build-syndrome: It has headers but they're missing definitions, and .c files are missing includes.
If my clean builds were 30 seconds I’d prefer incremental performance too. Our precompiled headers take longer than that. A clean ue5 editor build with precompiled headers, adaptive unity builds and optimisations disabled takes 20 minutes or so. Incremental changes to game projects are usually prettt quick - under 5 seconds or so (which is fast enough when the whole thing takes 30+ seconds to boot anyway.
> Unfortunately raddbg suffers from unity-build-syndrome: It has headers but they're missing definitions, and .c files are missing includes.
We run a CI build once a week with unity disabled to catch these issues.
> Should be a minimum of 32x faster just building in parallel with our hardware ;)
My experience here is that parallelism is less than linear and avoiding repeated work is super-linear - there’s a happy medium. If you had 128 files and 32 cores it’s probably fast to compile in parallel, but likely even faster if you can just dispatch 32 “blobs” and link them at the end.
This is true. After making my earlier comment, I went home and tested MSVC and Clang and got similar numbers. I had 1.5s in my head from using it earlier but maybe some changes made it slower. Either way, it's a lot of code and stays on the order of seconds or tens of seconds rather than minutes.
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).
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.
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.
Rust does do this. The unit of compilation is the whole crate and the compiler creates appropriately sized chunks of LLVM IR to balance duplicate work and incrementality.
Rust is generally faster to compile on a per-sourceline basis than c++. But rust projects compile all their dependencies as well.
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'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.
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.
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...
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.
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.
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.
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.
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.
Slow compile times really mess with your brain. When I wanted to test two different solutions, I would keep multiple separate clones (each one takes about 80GB, mind you) and do the manual equivalent of branch prediction by compiling both, just in case I needed the other one as well.
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.
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.
441 comments
[ 3.2 ms ] story [ 177 ms ] thread> 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.
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.
Don't do what you don't need to do.
They are already long past the point of "complicate things unnecessarily".
A simple Dockerfile pales in comparison.
Docker is a (the, in some areas) modern way to do it, but far from the only way.
no, that's sandboxing.
"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.
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.
In my own time I usually write C or Zig.
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.
When developing locally, use `cargo test` in your cli. When deploying to the server, build the Docker image on CI. If it takes 5 minutes to build it, so be 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.
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?
For me, the ideal debugger is one you never leave. That is you are in a debugging session, maybe with your code running, or stopped on a breakpoint, and you write your code without leaving the session. You can see the values of your variables as you type them, the branches that will be taken, etc... When an error condition happen, you break on it, and still without leaving the session, you can fix the code, roll back before the error happened and see if it passes. IntelliJ is close to that, and it seems like some tools from the video game industry are even better.
Debugging should be natural, it shouldn't be something you pull out as a last resort. If it is not natural, it is a bad debugger, or a bad development environment, period. Maybe there are reasons for why it is bad, maybe the language designers have other priorities, which is acceptable, but it doesn't change the fact that it is bad. Same goes for slow compile times by the way.
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)
I've only ever really used a debugger on embedded, we used gdb there. I know VS: Code has a debugger that works, I'm sure other IDEs do too.
> edit and continue
Hard to do in a pre-compiled language with no runtime, if you're asking about what I think you're asking about.
> Hot reload
Other folks gave you good links, but this stuff is pretty new, so I wouldn't claim that this is great and often good and such.
> Full IDE
I'm not aware of Rust-specific IDEs, but many IDEs have good support for Rust. VS: Code is the most popular amongst users according to the annual survey. The Rust Project distributes an official LSP server, so you can use that with any editor that supports it.
And I mentioned it as a downside of C++ on Linux, and I would expect a language that has "the best" tooling to have that.
C++ tooling isn't that great, but it has one thing going for it: it is popular in the video game industry, and the video industry has some of the best tools.
And sure enough, if by tooling you mean "package management", I'd say everything is better than C++, and on the other side, it seems that cargo is pretty good. I don't know how they tackle the "left-pad" problem that plagues npm though. By that I mean supply-chain attacks.
Btw, left-pad fallout wasn't all that bad. It's not like the author put something malicious into the code. For less than a day, people couldn't download that dep from npm. If someone really needed to fix a build, they could copy in a backup. Pretty sure a typical C++ or Python project build gets broken on its own more often than that.
Idk either, but code audits are definitely a solution. Take Debian packages for instance. Debian has package maintainers, and while they may no do full audits, they will at least test it before publishing. In addition, it doesn't get in the "stable" release before an extensive testing phase. Security patches are usually backported.
Or do like with the Apple App Store, where you don't get to publish anything unreviewed.
These are not perfect solution, there is no such thing as a perfect solution. For instance, Debian is famously lagging behind in versions, and the App Store will sometimes reject your app for no good reason, while being expensive. In every case there is some barrier to entry, a slow process, and it costs time and money, but that mitigates some of the issues.
Npm seems to have very little safeguards, has a culture of always taking the latest version, and as a result is often victim to supply-chain attacks. I don't think it is just popularity. Debian is really popular too, but AFAIK, it doesn't have this problem, in fact, one of the best known supply-chain attack is the xz library, and Debian didn't fall to it.
A.k.a. "Remember the Vasa!" https://news.ycombinator.com/item?id=17172057
New features: yes
Talking to users and fixing actual problems: lolno, I CBF
Thanks to pre-compiled headers, incremental compiler, incremental linker, and using binary libraries for all dependencies. Nowadays modules as well.
Rust compiles from scratch, some crates might get compiled multiple times due to conflicting configurations on the dependency graph, binary libraries require additional setup for something like scache.
In C/C++ I don't even have to worry about it.
Out of interest what were they?
The one that immediately comes to mind is cvc5... not super recently though.
I suspect that "tried to" is doing a bit of work here. The fact that it was failing and swapping out probably meant that the more memory heavy g++ processes were going slower than the memory light ones, resulting in more of them running simultaneously than would likely have happened in a normal successful build. Still, this was on a system with 32GB of ram, so it was using roughly that before swapping would slow down more memory intensive 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 correct, but slinging cheap shots at each other is not how discussions on this site are supposed to be.
> I think this thread needs a little more nuance.
Yes, but your comment offers none.
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.
this is a terrible look for your whole community
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.
https://bitemyapp.com/blog/rebuilding-rust-leptos-quickly/
https://old.reddit.com/r/rust/comments/1i2pr2e/improve_rust_...
https://old.reddit.com/r/rust/comments/ua09tc/experimental_f...
https://old.reddit.com/r/rust/comments/1k9ihhn/does_breaking...
https://old.reddit.com/r/rust/comments/x9z4cm/speeding_up_in...
https://old.reddit.com/r/rust/comments/rlszeq/the_best_cpu_f...
https://old.reddit.com/r/rust/comments/1hpuy01/why_you_need_...
https://old.reddit.com/r/rust/comments/1h9bdbr/rust_llvm_by_...
https://old.reddit.com/r/rust/comments/1j1rvy1/help_me_under...
Anyhoodle, I'm looking forward to testing the compile times again when Zig says the build times are fast now.
But if we're talking about incrementality, I look forward to testing that too when it's ready. I've got a Bazel build for work that pulls together Rust along with a custom (very slow) compiler and the cached successful pipelines take 2-3 minutes (mostly the benchmark run) and the ones that churned the non-rust take 15-18 minutes, of which about 10-11 minutes is just compiling the non-rust.
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.
Do you have a list off the top of your head/do you know of a decent list? I've now read many "compiler slow" thoughtpieces by many people and I have yet to see someone point at a specific feature and say "this is just intrinsically harder".
I believe that it likely exists, but would be good to know what feature to get mad at! Half joking of course
You can't have a language with 100% of the possible runtime perf, 100% of the possible compile speed and 100% of the possible programmer ease-of-use.
At best you can abuse the law of diminishing returns aka the 80-20 rule, but that's not easy to balance and you run the risk of creating a language that's okay at everything, but without any strong selling points, like the stellar runtime performance Rust is known for.
So a better way to think about it is: Given Rust's numerous benefits, is having subpar compilation time really that big of a deal?
As someone who uses Rust as a daily driver at work at zed.dev (about 600K LoC of Rust), and Zig outside of work on roc-lang.org (which was about 300K LoC of Rust before we decided to rewrite it in Zig, in significant part because of Rust's compilation speed), yes - it is an absolutely huge deal.
I like a lot of things about Rust, but its build times are my biggest pain point.
It leads to blithe things about how there's no such things as zero-cost abstractions. But at one point the cost is so low and amortized that you're looking at something that's basically free.
What is the compilation cost of the `?` syntax shorthand? Probably quite low!
What about the compilation cost of underscores in number literals? Again, quite low?
To this point a bit, I think a lot of people talk about the borrow checker, but my understanding is it's fairly low cost, and I don't worry about it.
Monomorphisation costs though? That feels like something that could generate a lot of work.
Let's dig into this bit of that, to give you some more color:
> 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.
Rust decided to go with the classic separate compilation model that languages like C use. Let's talk about that compared to Zig, since it was already brought up in this thread.
So imagine we have a project, A, and it depends on B. B is a huge library, 200,000 lines of code, but we only use one function from it in A, and that function is ten lines. Yes, this is probably a bad project management decision, but we're using extremes here to make a point.
Cargo will compile B first, and then A, and then link things together. That's the classic model. And it works. But it's slow: rust had to compile all 200,000 lines of code in B, even though we only are gonna need ten lines. We do all of this work, and then we throw it away at the end. A ton of wasted time and effort. This is often mitigated by the fact that you compile B once, and then compile A a lot, but this still puts a lot of pressure on the linker, and generics also makes this more complex, but I'm getting a bit astray of the main point here, so I'll leave that alone for now.
Zig, on the other hand, does not do this. It requires that you compile your whole program all at once. This means that they can drive the compilation process beginning from main, in other words, only compile the code that's actually reachable in your program. This means that in the equivalent situation, Zig only compiles those ten lines from B, and never bothers with the rest. That's just always going to be faster.
Of course, there are pros and cons to both of these decisions, Rust made the choice it did here for good reasons. But it does mean it's just going to be slower.
I suppose a lot of the coherence checking still "needs" to happen, but how I would love a "--i-dont-care-about-coherence-in-dependent-traits" flag or something. I know I'm missing a lot
There is ino operator overloading like C, so A + B can only mean one thing.
You can't redeclare a variable, so foo can only map to one thing.
The list goes on.
Basically it was designed to compile faster, and that means many issues on Github have been getting rejected in order to keep it that way. It's full of compromises.
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.
> Fil-C is currently 1.5x slower than normal C in good cases, and about 4x slower in the worst cases.
with room for optimization still. Compatibility has improved massively too, due to big changes to how it works. The early versions were kind of toys, but if Filip's claims about the current version hold up then this is starting to look like a very useful bit of kit. And he has the kind of background that means we should take this seriously. There's a LOT of use cases for taking stuff written in C and eliminating memory safety issues for only a 50% slowdown.
Your first claim is unverifiable and the second one is just so, so wrong. Even big projects with very talented, well-paid C or C++ devs eventually end up with CVEs, ~80% of them memory-related. Humans are just not capable of 0% error rate in their code.
If Zig somehow got more popular than C/C++, we would still be stuck in the same CVE bog because of memory unsafety. No thank you.
Zig does a lot of things to prevent or detect memory safety related bugs. I personally haven't encountered a single one so far, while learning the language.
> ~80% of them memory-related.
I assume you're referencing the 70% that MS has published? I think they categorized null pointer exceptions as memory safety bugs as well among other things. Zig is strict about those, has error unions, and is strict and explicit around casting. It can also detect memory leaks and use after free among other things. It's a language that's very explicit about a lot of things, such as control flow, allocation strategies etc. And there's comptime, which is a very potent tool to guarantee all sorts of things that go well beyond memory safety.
I almost want to say that your comment presents a false dichotomy in terms of the safety concern, but I'm not an expert in either Rust or Zig. I think however it's a bit broad and unfair.
Until you guys write an actual formal specification, the compiler is the language.
The project is adopting Ferrocene for the spec.
Yes, the soundness hole itself is low impact and doesn't need to be prioritized but it undermines the binary "Zig is definitively not memory-safe, while safe Rust, is, by definition, memory-safe" argument that was made in response to me. Now you're dealing with qualitative / quantitative questions of practical impact, in which my original statement holds: "Zig is less memory safe than Rust, but more than C/C++. Neither Zig nor Rust is fundamentally memory safe."
You can of course declare that Safe Rust is by definition memory safe, but that doesn't make it any more true than declaring that Rust solves the halting problem or that it proves P=NP. RustBelt is proven sound. Rust by contrast, as being documented by Ferrocene, is currently fundamentally unsound (though you won't hit the soundness issues in practice).
- If a safe Rust program exhibits a memory safety problem, it is a bug in the Rust compiler that is to be fixed - If a Zig program exhibits a memory safety problem, it is a bug in the Zig program that needs to be fixed, not in the compiler
Wouldn't you agree?
> Ferrocene is intended to document the behavior of the current version of the rustc compiler, so it's just an effort to formalize "the compiler is the language".
I must admit I haven't read the specification, but I doubt they attempt to be "bug for bug" compatible in the sense that the spec enumerates memory safety bugs present in the Rust compiler. But am I then mistaken?
As to the Ferrocene specification, it explicitly states "Any difference between the FLS and the behavior of the Rust compiler is considered an error on our part and the FLS will be updated accordingly."
Proposals to fix the soundness hole in Rust either change the variance rules themselves, or require where clauses in certain places. Either of these changes would require corresponding changes to chapter 4 of the Ferrocene specification.
I really don’t see how you can claim with a straight face that the two approaches are the same.
Look, I'm not saying the Zig and Rust approaches are the same. I explicitly stated that Rust is more memory safe than Zig (which is in turn more memory safe than C/C++).
This is because Rust has clearly delineated a "safe" subset of Rust which you have to explicitly opt out of that is mostly sound (and has a goal of eventually being entirely sound), has a culture of encouraging the use of the safe subset, and has taken a good approach to the interfacing of safe and unsafe code (i.e. if unsafe code is properly written and satisfies the exposed contract - despite the compiler being unable to verify this - then safe code can safely be linked with it).
All of this results in extremely low risk of memory corruption for Rust programs in practice (far lower than any other commonly used non-GC language with the sole exception of SPARK).
What you can't do though is reject the notion of memory safety being a sliding scale and draw a binary distinction between languages that are 100% perfectly memory safe and languages that are memory unsafe. Well you can, but Rust will fall on the side of memory unsafe for many years to come. Java (ignoring vendor-specific extensions) falls on the safe side though - the language semantics as specified are sound and it doesn't even have an unsafe subset.
Look, Zig has neat ideas. No disputing that. But on the sliding scale there’s a threshold where people are comfortable classifying something as being memory safe to avoid perfect being the enemy of good. And by that classification criteria, Zig falls well below the bar and Rust clears it easily.
Right, this is from before it's adopted as the actual spec, because it was from outside the project, and so could not be.
Also, these goalposts are moving: it was "Rust doesn't have a spec" and now it's "I don't like the spec."
Fixing this soundness hole does not require a breaking change to the language. It is an implementation bug, not a problem with the language as specified. But even if it were, Rust's policies around soundness do allow for this, and the project has done it in the past.
If there is a proposed fix to the soundness hole that wouldn't reject some existing sound Rust code, please link to it; none of the proposed fixes I've seen do so. And yes, Rust's policies do allow for breaking changes in pursuit of soundness - likely some day many years from now safe Rust will indeed be sound and guaranteed to be memory safe.
That is the absolute best description of memory safety I’ve heard expressed.
[1] The cult of the orange man would call this a plurality, which may be what has tripped you up, but the civilized world calls it a majority.
But by implementation and spec definitely not.
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.
[0]: https://news.ycombinator.com/item?id=44369642
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.
But not having to is a win, as the monomorphised sorts are just much faster at runtime than having to do an indirect call for each comparison.
Not all programmers of course - if you look at std there are many places that split types into generic and non-generic parts so the compiler will reuse as much code as possible, but it does come at the cost of additional complexity. Worse if you aren't already aware of why they are doing it, the language does a marvellous job of hiding the reason that complexity is there. I'd wager a novice Rust programmer is as befuddled by it as a JavaScript programmer coming across his first free() call in C.
I have this dream of a language like Rust that makes the trade-off plain, so the programmer is always aware of "this is a zero cost abstraction - you're just making it plain via the type system your doing the right thing" and "I'm going to have to generate a lot of code for this". Then go a step further and put the types and source you want to export to other libraries in a special elf section in the .so so you don't need the source to link against it, then go another step further and make the programmer using the .so explicitly instantiate any stuff that does require a lot of code generated so he is aware of what is happening.
That said, I don't think it would help the compile time problem in most cases. C++ already does something close by forcing you to put exported stuff in .h files, and they ended up with huge .h files and slow compiles anyway.
Nevertheless doing that would make for a Rust like language, that, unlike Rust, supported an eco-system of precompiled libraries just like C does. Rust is so wedded to transparent monomorphisation it looks near impossible now.
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.
There are multiple caveats on providing this to users (we can't assume that macro invocations are idempotent, so the new behavior would have to be opt in, and this only benefits incremental compilation), but it's in our radar.
Unfortunately that doesn't seem to ever be a scenario cargo will support out of the box.
I belong to the school of thought that C style macros in C++ should be nuked.
Exception being source code I don't own.
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.
I’d like to see tooling for this to pinpoint bottlenecks - it’s not always obvious what’s making builds slow.
I second this enthusiastically.
If it improves compile time, that sounds like a bug in the compiler or the design of the language itself.
Even this can lead to unworkable compile times, to the point that code is rewritten.
Nah. Slow type checking in Swift is primarily caused by the fact that functions and operators can be overloaded on type.
Separately-compiled generics don't introduce any algorithmic complexity and are actually good for compile time, because you don't have to re-type check every template expansion more than once.
Wouldn't you say a lot of that comes from the macros and (by way of monomorphisation) the type system?
I suspect this leaks into both compile-time and run-time costs.
Could you expand on that, please? Every time you run dynmically linked program, it is linked at runtime. (unless it explicitly avoids linking unneccessary stuff by dlopening things lazily; which pretty much never happens). If it is fine to link on every program launch, linking at build time should not be a problem at all.
If you want to have link time optimization, that's another story. But you absolutely don't have to do that if you care about build speed.
I think lazily linking is the default even if you don't use dlopen, i.e. every symbol gets linked upon first use. Of course that has the drawback, that the program can crash due to missing/incompatible libraries in the middle of work.
Anyway, while what you said is theoretically half-true, a fairly large number of libraries are not designed/encapsulated well. This means almost all of their symbols are exported dynamically, so, the idea that there are only "few public exported symbols" is unfortunately false.
However, something almost no one ever mentions is that ELF was actually designed to allow dynamic libraries to be fairly performant. It isn't something I would recommend, as it breaks many assumptions on Unices, (while you don't get the benefits of LTO) you can achieve code generation almost equivalent to static linking by using something like "-fno-semantic-interposition -Wl,-Bsymbolic,-z,now". MaskRay has a good explanation on it: https://maskray.me/blog/2021-05-16-elf-interposition-and-bsy...
Go got famous compile times, because for a decade a new generation educated in scripting languages and used to badly configured C and C++ projects, took for innovation, what was actually a return to old values in compiler development.
DLLs got their start when early windowing systems didn't quite fit on the workstations of the era in the late 80s / early 90s.
In about 4 minutes both Microsoft and GNU were like, "let me get this straight, it will never work on another system and I can silently change it whenever I want?" Debian went along because it gives distro maintainers degrees of freedom they like and don't bear the costs of.
Fast forward 30 years and Docker is too profitable a problem to fix by the simple expedient of calling a stable kernel ABI on anything, and don't even get me started on how penetrated everything but libressl and libsodium are. Protip: TLS is popular with the establishment because even Wireshark requires special settings and privileges for a user to see their own traffic, security patches my ass. eBPF is easier.
Dynamic linking moves control from users to vendors and governments at ruinous cost in performance, props up bloated industries like the cloud compute and Docker industrial complex, and should die in a fire.
Don't take my word for it, swing by cat-v.org sometimes and see what the authors of Unix have to say about it.
I'll save the rant about how rustc somehow manages to be slower than clang++ and clang-tidy combined for another day.
https://www.youtube.com/watch?v=dOfucXtyEsU
…
Dynamic linking moves control from users to vendors and governments at ruinous cost in performance, props up bloated industries...
This is ridiculous. Not everything is a conspiracy!
If fact, if there was anything remotely controversial about a bunch of extremely specific, extremely falsifiable claims I made, one imagines your rebuttal would have mentioned at least one.
I said inflmatory things (Docker is both arsonist and fireman at ruinous cost), but they're fucking true. That Alpine in the Docker jank? Links musl!
https://en.wikipedia.org/wiki/Advanced_persistent_threat
https://en.wikipedia.org/wiki/Operation_Olympic_Games
https://simple.wikipedia.org/wiki/Stuxnet
https://en.wikipedia.org/wiki/Cozy_Bear
https://en.wikipedia.org/wiki/Fancy_Bear
https://en.wikipedia.org/wiki/Tailored_Access_Operations
But people should make an informed choice, and there isn't any noble or high minded or well-meaning reason to try to shout that information down.
Don't confidently assert falsehoods unless you're prepared to have them refuted. You're entitled to peddle memes and I'm entitled to reply with corrections.
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.
For pure computational workloads, it'll be faster. However, anything with heavy allocation will suffer as apparently the gccgo GC and GC related optimizations aren't as good as cgo's.
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.
Unless you use sqlite, in which case your build takes a million years.
Compilation speed depends on what you do with a language. "Fast" is not an absolute, and for most people it depends heavily on community habits. Rust habits tend to favor extreme optimizability and/or extreme compile-time guarantees, and that's obviously going to be slower than simpler code.
Go and Dlang compilers were designed by those that are really good at compiler design and that's why they're freaking fast. They designed the language around the compiler constraints and at the same managed to make the language intuitive to use. For examples, Dlang has no macro and no unnecessary symbols look-up for the ambiguous >>.
Because of these design decisions both Go and Dlang are anomaly for fast compilation. Dlang in particular is notably more powerful and expressive compared to C++ and Rust even with its unique hybrid GC and non-GC compilation.
In automotive industry it's considered a breakthrough and game changing achievement if you have a fast transmission for seamless auto and manual transmission such as found in the latest Koenigsegg hypercar [1]. In programming industry however, nobody seems to care. Walter Bright the designer of Dlang has background in mechanical engineering and it shows.
[1] Engage Shift System: Koenigsegg new hybrid manual and automatic gearbox in CC850:
https://www.topgear.com/car-news/supercars/heres-how-koenigs...
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
But it _is_ about the sheer volume of stuff passed to LLVM, as you say, which comes from a couple of places, mostly related to monomorphization (generics), but also many calls to tiny inlined functions. Incidentally, this is also what makes many "modern" C++ projects slow to compile.
In my experience, similarly sized Rust and C++ projects seem to see similar compilation times. Sometimes C++ wins due to better parallelization (translation units in Rust are crates, not source files).
As an example, say your function takes anything that can be turned into a String. You'd write a generic wrapper that does the ToString step, then change the existing function to just take a String. That way when your function is called, only the thin outer function is monomorphised, and the bulk of the work is a single implementation.
It's not _that_ commonly known, as it only becomes a problem for a library that becomes popular.
* 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).
I wouldn't like it that much
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.
Same applies to point 3, when used alongside external templates.
https://github.com/rust-lang/rustc_codegen_cranelift
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.
Most likely treating C and C++ as scripting languages with header only libraries, only to complain about build times afterwards.
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.
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?
EDIT: i signed up to get access to unreal so take a look at how they do unity builds and turns out they have their own build tool (not CMake) that orchestrates the build. so does anyone know (can someone comment) whether unity builds for them (unreal) means literally one file for literally all project sources files or if it's "higher-granularity" like UNITY_BUILD in CMake (i.e., single file per object).
UE doesn't use a full unity build, it groups some files together into small "modules". I can see how this approach may have some benefits; you're trading off a faster clean build for a slower incremental build.
I tested compiling UnrealFrontend, and a default setup with the hybrid unity build took 148s. I noticed it was only using half my cores due to memory constraints. I disabled unity and upped the parallelism and got 182s, so 22% slower while still using less memory. A similarly configured unity build was 108s, so best case is ~2x.
On the other hand only changing the file TraceTools/SFilterPreset.cpp resulted in 10s compilation time under a unity build, and only 2s without unit.
I can see how this approach has its benefits (and drawbacks). But to be clear this isn't what projects like raddbg and sqlite3 are doing. They're doing a single translation unit for the entire project. No parallelism, no incremental builds, just a single compiler invocation. This is usually what I've seen people mean by a unity build.
> 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?
I just did a release build of egui in 35s, about the same as raddbg's release build. This includes compiling dependencies like wgpu, serde and about 290 other dependencies which add up to well over a million lines of code.
Note I do have mold configured as my linker, which speeds things up significantly.
I’m the tech director for a game studio using unreal and we spec the dev machines with enough memory to avoid that pressure - we require 2.5GB/Core, rounded up. Right now we’re using i9 14900k’s with 128GB RAM. We were on 5950x with 64GB before this.
I tried egui last night but failed to build it due to missing dependencies, but I’ll see if I can get it running on Monday. I’d also love to see what raddbg would be like if we split it into even 4 compilation units
Most certainly an unfair comparison; I did not know that "unity build" also referred to a hybrid setup. I've only seen people advocate for a single translation unit approach, and that's always just plain slow.
Personally I'll almost always prefer faster incremental compilation; thankfully my work's clean builds only take 30 seconds.
> I’d also love to see what raddbg would be like if we split it into even 4 compilation units
Should be a minimum of 32x faster just building in parallel with our hardware ;)
Unfortunately raddbg suffers from unity-build-syndrome: It has headers but they're missing definitions, and .c files are missing includes.
> Unfortunately raddbg suffers from unity-build-syndrome: It has headers but they're missing definitions, and .c files are missing includes.
We run a CI build once a week with unity disabled to catch these issues.
> Should be a minimum of 32x faster just building in parallel with our hardware ;)
My experience here is that parallelism is less than linear and avoiding repeated work is super-linear - there’s a happy medium. If you had 128 files and 32 cores it’s probably fast to compile in parallel, but likely even faster if you can just dispatch 32 “blobs” and link them at the end.
https://codeload.github.com/EpicGamesExt/raddebugger/tar.gz/...
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.
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.
The simple truth is a C compiler doesn’t need to do very much!
Have you tried troubleshooting a compiler error in a unity build?
Yeah.
Rust is generally faster to compile on a per-sourceline basis than c++. But rust projects compile all their dependencies as well.
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...
Damn, this makes such a great ad.
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.
[0] https://news.ycombinator.com/item?id=44390972
But it’s also probable that 16 seconds was fairly early in development and it would get much worse from there.
Performance matters.
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.
1 https://doc.rust-lang.org/reference/const_eval.html