> The best way to speed it up appears to be organizing a codebase in many crates.
A "crate" in Rust is the unit of compilation. In C, a file is the unit of compilation. Rust just lets you have a compilation unit that's composed of more than one file (without having to resort to C-style textual inclusion). But if you want, you can certainly have one-file-per-crate, just like you would in C. And what's nice about having many crates is that crates forbid circular dependencies, which trivially enables coarse-grained parallelism in the build system. So yes, organizing a large codebase into crates is the best way to achieve parallelism, but that isn't something to be deplored (and strictly controlling circular dependencies is useful for comprehending large codebases in general).
The forbidding of circular dependencies is exactly what makes it hard to achieve parallelism! It means you have to draw nice clean module boundaries and split your compilation units there. Clean boundaries sound nice, except… what if the module is getting large? Can you just take half the module, ctrl-x, ctrl-v into a new file, and get faster compilation times without having to do any massive refactors?
In C, usually yes.
In C++, sometimes yes. It depends on how template-heavy the code is, but if you have some discipline you can keep most logic out of headers and thus easily splittable.
In Rust, almost always no, because of circular dependencies. You can try to work around it by adding `dyn Trait` everywhere, but that requires a lot of code changes and comes at big ergonomic costs (and a small runtime cost).
Which is why in practice, Rust compilation units are almost always larger than C++ or C compilation units. Rust can sometimes be competitive with C++ on compilation speed anyway, thanks to a smarter build system and not having to re-parse headers a billion times, but usually it's slower.
> Can you just take half the module, ctrl-x, ctrl-v into a new file, and get faster compilation times without having to do any massive refactors?…In Rust, almost always no, because of circular dependencies
This feels like a strange, overly-specific complaint.
It reads a bit like “When I write entangled code, it’s hard to untangle”. Like, yeah, the only thing that’ll save you from that is…not writing entangled code? I’m not of the opinion that the argument of “yeah but C lets me do whacky stuff” is a particularly strong line.
FWIW, letting a module grow, and then splitting modules up by cut-and-pasting stuff out along natural domain lines generally _is_ how I write Rust. Largely due to how easy it makes it to construct modules and submodules.
I may have been a little overly specific, since there are other issues besides cycles that also block splitting crates apart, but from what I’ve seen it’s very common for modules within a crate to have cyclic dependencies and therefore not be easily factorable into separate crates.
The industry accepted way of handling circular dependencies is to not have them and heavily lint against them in languages which permit them in compilation or runtime.
You’re thinking at the wrong scale. Rust allows circular dependencies just fine, within modules in a crate. And it’s extremely common for modules within a crate to have at least some circular dependencies - type X has some method (trait impl of inherent impl) that mentions type Y, and type Y has some method that mentions type X. In fact, I would be surprised if you could name me a single medium-size-or-larger popular crate that doesn’t have at least one cyclic reference between modules! Though, sometimes those cycles are not essential and could be avoided by splitting up modules. But sometimes they are. Either way, in C or C++ those modules would probably be their own compilation units.
That said, you also run into related parallelism blockers without cycles. For example, the orphan rules force most trait impls for a type to be in the same crate as the type definition. Also, a module which has no source-level cycles will often have cycles after monomorphization. In this case, Rust doesn’t prevent you from splitting the code into crates like it does with source-level cycles, but you do lose most of the actual codegen parallelization unless you can switch from generics to trait objects.
A "crate" in Rust is the unit of compilation. In C, a file is the unit of compilation. Rust just lets you have a compilation unit that's composed of more than one file (without having to resort to C-style textual inclusion). But if you want, you can certainly have one-file-per-crate, just like you would in C. And what's nice about having many crates is that crates forbid circular dependencies, which trivially enables coarse-grained parallelism in the build system. So yes, organizing a large codebase into crates is the best way to achieve parallelism, but that isn't something to be deplored (and strictly controlling circular dependencies is useful for comprehending large codebases in general).