3 ms·
Macros can be, but in part because they can produce new items (top level declarations, to sort of make the same handwave as the article does) and so that means
by steveklabnik 3mo ago
Macros can be, but in part because they can produce new items (top level declarations, to sort of make the same handwave as the article does) and so that means you have to do macro expansion and stuff before you can even start to check some things, and similar issues. See the link I posted above for some details on a related issue.
There's also stuff around name resolution.
Proc macros are just an inherently very slow way to do what they do.
Because Rust commits to the traditional compilation model and pipleine (which I think is overall a good thing, or at least, a good thing to support), it does a lot of work that will eventually be thrown away. Consider this example: I have a library with a function foo that returns a simple 42. I have a binary which calls foo from that library and prints the result. Now imagine the library is a hundred thousand lines of unrelated code to what the binary needs, but is useful for other people. Because compilation works in the "produce libraries, produce binary, link them all together" style model, you have to compile the entire library with all of that code, when all you need is really one function. That intermediate work is useful, and I'm picking an example that's deliberately extreme, of course.
There's a bunch of stuff like this, and I do not have time to really say more than that right now. But yeah, monomorphized generics also produce a lot of compile time pressure too, in various ways.
Anyway I just also want to reiterate a few things: first of all, all of these decisions were made for good reasons, and there are pros to what Rust does and why. It's just that compile times suffer because of it. What I wish was that we had taken compile times into more consideration when deciding what to do and why in a more serious way. The same decisions might have been made, but at least it would have been known, rather than the situation now, where there's just a tremendous amount of work to try to optimize what exists, rather than having the freedom to maybe tweak some things to make that job way easier.
- saghm 3mo ago> Because Rust commits to the traditional compilation model and pipleine (which I think is overall a good thing, or at least, a good thing to support), it does a lot of work that will eventually be thrown away. Consider this example: I have a library with a function foo that returns a simple 42. I have a binary which calls foo from that library and prints the result. Now imagine the library is a hundred thousand lines of unrelated code to what the binary needs, but is useful for other people. Because compilation works in the "produce libraries, produce binary, link them all together" style model, you have to compile the entire library with all of that code, when all you need is really one function. That intermediate work is useful, and I'm picking an example that's deliberately extreme, of course. I feel extremely validated reading this! I've had a pet theory for a while that compile times would be drastically reduced if every single item in a crate were implicitly under a cargo feature flag and then they only got enabled if they were imported (and used in non-dead code). I know that making something like that work isn't anywhere close to as simple as I'm describing it, and there are probably a million edge cases, but I've long felt that the ergonomics of Cargo features basically making it too annoying to expose everything conditionally (and then transitively expose all of the features from all of the direct dependencies as well so that things depending on your library could also only conditionally enable them) is secretly the reason that people think Rust compile times are slow, and seeing someone who has way more direct knowledge than me of how all of it works under the hood give a similar take makes me more confident that I might have been on to something all along.
- steveklabnik 3mo agoYes, this idea is sort of similar, just like, more complicated in a sense than the clean design, which is what Zig does. Incidentally, you might want to look at gc-sections, which Rust already does. As well as https://rust-lang.github.io/rust-project-goals/2025h2/relink-dont-rebuild.html https://rust-lang.github.io/rust-project-goals/2025h2/relink... which is kinda related. The sort of key here is understanding that "produce a library" means that every public item is "used" in the sense of "do we need to compile it." The real trick is to do demand-driven compilation starting from the actual final program's needs, and this is inherently at odds with the idea of producing standalone libraries and combining them into the final artifact.
- saghm 3mo agoMakes sense! I think what's most surprising to me about the library compilation model is that for the most part, it doesn't seem like it's actually that useful to how Rust does things by default. Without something like sccache, the intermediate libraries aren't going to be reused across all of my projects, so unless I'm compiling multiple binaries in the same project, I'm not really getting much out of having the entire library sitting there in my target directory rather than just the subset that I'm using. I'm guessing this is related to what you mean by potentially being able to do something differently if there were more time before 1.0?
- steveklabnik 3mo agoI think that there is a general desire to work with the system, and since that is how other systems languages have generally done it, doing it the same way feels good. You don't necessarily want to innovate on everything all at once. It is true that for various reasons, Rust can't take as much advantage as say, C can. > I'm guessing this is related to what you mean by potentially being able to do something differently if there were more time before 1.0? I mean more traditional language design things, but sure, this too.
- saghm 2mo ago
- xenadu02 2mo agoThe traditional model of compilation is the biggest issue holding back fast incremental compilation IMHO. Swift suffers from this as well. Even a simple change to one file results in re-parsing the whole library because definitions come from anywhere and we have to obey the 1970s single file compilation model. The result is a driver spawns 8 threads and each one wastes time re-parsing every file in the library looking for definitions. AFAIK Rust doesn't really track dependencies at the file or function level either so it doesn't really know what changed. To me compilers should be content-addressed databases. Each declaration and its associated content generate hashes that roll up to its containing type or namespace, then to the file, then to the library as a whole, along with hashes of the dependencies. Changing the type signature of a single function should result in the compiler being able to cheaply determine whether that has any visibility and if so to what other files in the same library or if it affects the public interface. A file that hasn't changed and whos inputs hasn't changed should re-use the IR from the prior compilation. Even for an individual type that should be the case so changing the internals of a function in a struct only regnerates that one function and nothing else. The compiler knows deterministically that change can't have affected anything else. That has major benefits for code completion and editing as prior compilations can feed into generating errors or suggested corrections. Then you can take things a step further and JIT a changed function, injecting the new machine code on the fly so long as the shapes of the types don't change. Very useful for debugging. Compilers are mostly held back because the people who write compilers are stuck on certain ideas about how compilers should be written.
- panstromek 2mo ago> To me compilers should be content-addressed databases. There's a language called Unison that does that - and the "content" is the AST, so all functions that have the same shape are the same function. It's pretty interesting.
- SkiFire13 2mo ago> To me compilers should be content-addressed databases Rustc already does something like this. The issues are: - when you have to rehash everything to check that they indeed didn't change from the previous compilation. For big projects this takes _a lot_ of time. - when small changes do indeed change the hash of a lot of seemingly unrelated code, which is more common than you might think.
- dminik 2mo agoA really dumb 1 AM question. If there is a lot of work thrown away because stuff is compiled even if not needed, would making every function generic and delaying compilation until instantiation help here? Note that it's not a serious suggestion, but I wonder what effect it would have on build times.
- steveklabnik 2mo agoI mean, the core thing is like, you have to have a compiler codebase (and language semantics) that's designed around being able to delay in the first place to be able to even try this, and once you've gotten that in place, well, it's not really about this specific idea anymore.
- tancop 2mo agoWould this work better in a language where generics are vtable based by default and only monomorphized as a form of LTO? If you have a Zig style machine IR where function calls are a "fake instruction" you can decide between direct and virtual call at final codegen time. That would mean generics in shared libraries are possible without hacks but you can always choose to statically link and monomorphize for speed. Libraries could even ship both the vtable based catch all code and specializations for common types as a fast path (but that would probably need a custom dynamic linker).
- steveklabnik 2mo agoIt really depends on a lot of things. This design is possible, yes, but it also means that you can't have some of the same features as Rust has. Rust's traits can be "non-dyn safe" (which used to be called "non-object safe"), because sometimes you cannot use them via a vtable, and must use them via monomorphization. So you can have languages that do things this way, but they make other tradeoffs (either performance ones or power ones, depending).
- panstromek 2mo agoYes, and there is some compiler flag to do this, even though it's not 100% intended for this use (I think it's something like mir-inline-trheshold=0). There's also -Zhint-mostly-unused flag. The tradeoff is that these functions then have to be encoded in metadata for downstream crates, so it's not necessarily faster.