Compiler performance must be considered up front in language design. It is nearly impossible to fix once the language reaches a certain size without it being a priority. I recently saw here the observation that one can often get a 2x performance improvement through optimization, but 10x requires redesigning the architecture.
Rust can likely never be rearchitected without causing a disastrous schism in the community, so it seems probable that compilation will always be slow.
Many of complaints towards Rust, or C++, are in reality tooling complaints.
As shown on other ecosystems, the availability of interpreters or image based tooling are great ways to overcome slow optimizating compilers.
C++ already had a go at this back in the early 90's with Energize C++ and Visual Age for C++ v4, both based on Common Lisp and Smalltalk from their respective owners.
They failed on the market due to the hardware requirements for 90's budgets.
Now slowly coming back with tooling like Visual C++ hot reload improvements, debugging optimised builds, Live++, Jupiter notebooks.
Rational Software started their business selling Ada Machines, the same development experience as Lisp Machines, but with Ada, lovely inspired on Xerox PARC experience with Mesa and Mesa/Cedar.
Haskell and OCaml, besides the slow compilers, have bytecode interpreters and REPLs.
D has the super fast dms, with ldc and gdc, for the optimised builds suffering from longer compile times.
So while Rust cannot be archited in a different way, there is certainly plenty of room for interpreters, REPLs, not compiling always from source and many other tooling improvements, within the same language.
I had a coworker who was using Rational back then, and found out one of its killer features was caching of pre compiled headers. Whoever changed them had to pay the piper of compilation, but everyone else got a copy shipped to them over the local network.
It's certainly possible to think of language features that would preclude trivially-achievable high-performance compilation. None of those language features that are present in Rust (specifically, monomorphized generics) would have ever been considered for omission, regardless of their compile-time cost, because that would have compromised Rust's other goals.
There are many more mundane examples of language design choices in rust that are problematic for compile time. Polymorphization (which has big potential to speed up compile time) has been blocked on pretty obscure problems with TypeId. Procedural macros require double parsing. Ability to define items in function bodies prevents skipping parsing bodies. Those things are not essential, they could pretty easily be tweaked to be less problematic for compile time without compromising anything.
This is an oversimplification. Automatic polymorphization is blocked on several concerns, e.g. dyn safety (and redesigning the language to make it possible to paper over the difference between dyn and non-dyn safe traits imposes costs on the static use case), and/or obscure LLVM implementation deficiencies (which was the blocker for the last time I proposed a Swift-style ABI to address this). Procedural macros don't require double-parsing; many people do use syn to parse the token stream, but 1) parsing isn't a performance bottleneck, 2) providing a parsed AST rather than a token stream freezes the AST, which is something that the Rust authors deliberately wanted to avoid, rather than being some kind of accident of design, 3) at any point in the future the Rust devs could decide to stabilize the AST and provide a parsed representation, so this isn't anything unfixable that would cause any sort of trauma in the community, 4) proc macro expansions are trivially cacheable if you know you're not doing arbitrary I/O, which is easy to achieve manually today and should absolutely be built-in to the compiler (if for no other reason than having a sandboxed dev environment), but once again this is easy to tack on in future versions. As for allowing item definitions in function bodies, I want to reiterate that parsing is not a bottleneck.
AIUI, "Swift-style" ABI mechanisms are heavily dependent on alloca (dynamically-sized allocations on the stack) which the Rust devs have just proposed backing out of a RFC for (i.e. give up on it as an approved feature for upcoming versions of Rust) because it's too complex to implement, even with existing LLVM support for it.
Indeed, an alloca-heavy ABI was what I proposed, and I'm aware that the Rust devs have backed away from unsized locals, but these are unrelated. I was never totally clear on the specific LLVM-related problem with the former (it can't be totally insurmountable, because Swift), but people more knowledgeable in LLVM than I seemed uneasy about the prospect. As for the latter, it's because the precise semantics of unsized locals are undetermined, and it's not clear how to specify them soundly (which carries a lot of weight coming from Ralf Jung).
Why does it have to be soundly specified though? Why not just provide an unsafe feature that works the same as existing C/LLVM, and "leave no room for a lower-level language"?
The safe featureset around it can always come later if the issues around how to specify it are worked out.
The difficulty is that `unsafe` doesn't mean "yolo", it means "there are memory safety invariants here that you, the programmer, must manually uphold", so we still need to consider what those invariants would be. I'm sure that Ralf Jung would be happy to talk more about this if anyone has any ideas for how to move forward:
"With #111374, unsized locals are no longer blatantly unsound. However, they still lack an actual operational semantics in MIR -- and the way they are represented in MIR doesn't lend itself to a sensible semantics; they need a from-scratch re-design I think. We are getting more and more MIR optimizations and without a semantics, the interactions of unsized locals with those optimizations are basically unpredictable. [...] If they were suggested for addition to rustc today, we'd not accept a PR adding them to MIR without giving them semantics. Unsized locals are the only part of MIR that doesn't even have a proposed semantics that could be implemented in Miri. (We used to have a hack, but I removed it because it was hideous and affected the entire interpreter.) I'm not comfortable having even an unstable feature be in such a bad state, with no sign of improvement for many years. So I still feel that unsized locals should be either re-implemented in a well-designed way, or removed -- the current status is very unsatisfying and prone to bugs."
> However, they still lack an actual operational semantics in MIR -- and the way they are represented in MIR doesn't lend itself to a sensible semantics
That's an issue with how the MIR for this feature has been defined, not with the feature itself. The claim that the implementation should be reworked from the ground up is one that I might agree with, but the recent proposal to back out of an existing RFC suggests that the devs see alloca itself as problematic. And that's bad news if you intend to use alloca throughout as a foundation for your Swift-like ABI support...
Just to clarify - polymorphization is not automatic dyn dyspatch or anything related. I'm talking about compile time optimization that avoids duplicating generic functions.
Yes, I bring up dyn because the most straightforward way to implement polymorphization would be to conceptually replace usages of T: Trait with dyn Trait.
> Macros themselves are a terrible hack to work around support for proper reflection.
No, I'm not sure where you got this idea. Macros are a disjoint feature from reflection. Macros exist to let you implement DSLs and abstract over syntax.
If you look at how macros are mostly used, though, a lot of that stuff could be replaced directly with reflection. Most derive macros, for example, aren't really interested in the syntax of the type they're deriving for, they're interested in its shape, and the syntax is being used as a proxy for that. Similarly, a lot of macros get used to express relationships between types that cannot be expressed at the type system level, and are therefore expressed at a syntactic level - stuff like "this trait is derived for all tuples based on a simple pattern".
There are also proc macros just for creating DSLs, but Rust is already mostly expressive enough that you don't really need this. There are some exceptions, like sqlx, that really do embed a full, existing DSL, but these are much rarer and - I suspect - more of a novelty than a deeply foundational feature of Rust.
But the point is that if you've got reflection (and an expressive base language, and a powerful enough type system, etc), you probably don't need macros. They're a heavy mallet when you almost always need a more precise tool. And the result of using macros is almost always worse than using that more precise tool - it will be harder to debug, it will play worse with tools like LSPs, it will be more complicated to read and write, it will be slower, etc.
I think macros are a necessarily evil in Rust, and I use them myself when writing Rust, but I think it's absolutely fair to judge macros harshly for being a worse form of many other language features.
No disagreement on your point, but this is a different argument than claiming that macros are an ugly hack to workaround lack of reflection.
Because Rust lacks reflection macros are used to provide some kind of ad-hoc reflection support, that much we agree... but macros are also used to provide a lot of language extensions other than reflection support. Macros in general exist to give users some ability to introduce new language features and fill in missing gaps, and yes reflection is one of those gaps. Variadics are another gap, some error handling techniques is yet another, as are domain specific languages like compile time regex! and SQL query macros.
But the point is that almost all of the common places where macros are used in everyday Rust could be replaced by reflection. There are exceptions like some of the ones you mention, but these are clever hacks rather than materially useful. Yes, you can write inline SQL and get it type checked, but you can also use a query builder or included strings and get the same effects but in a much less magical and brittle package.
Macros in Rust are primarily a tool to handle missing reflection capabilities, and them enabling other code as well is basically just a side effect of that.
They are disjoint, but the things you can use them for overlap a lot. In particular, I'd dare say a majority of existing `#[derive]`-style macros might be easier to implement in a hypothetical reflection layer instead.
Instead of taking a raw token stream of a struct, parsing it with `syn` (duplicating the work the compiler does later), generating the proper methods and carefully generating trait checks for the compiler to check in a later phase (for example, `#[derive(Eq)] struct S(u16)` creates an invisible never-called method just to do `let _: ::core::cmp::AssertParamIsEq<u16>;` so the compiler can show an error 20s after an incorrectly used macro finished), just directly iterate fields and check `field.type.implements_trait(Eq)` inside the derive macro itself.
That said, that's just wishful thinking - with how complex trait solving is, supporting injecting custom code in the middle of it (checking existing traits and adding new trait impls) might make compile time even worse, assuming it's even possible at all. It’s also not a clear perf win if a reflection function were to run on each instantiation of a generic type.
It's a "unit" in the sense of calling `rustc` once, but it's not a minimal unit of work. It's not directly comparable to what C does.
Rust has incremental compilation within a crate. It also splits optimization work into many parallel codegen units. The compiler front-end is also becoming parallel within crates.
The advantage is that there can be common shared state (equivalent of parsing C headers) in RAM, used for the entire crate. Otherwise it would need to be collected, written out to disk, and reloaded/reparsed by different compiler invocations much more often.
> Rust has incremental compilation within a crate. It also splits optimization work into many parallel codegen units.
Eh, it does, but it's not currently very good at this in my experience. Nothing unfixable AFAIK (and the parallel frontend can help (but is currently a significant regression on small crates)), but currently splitting things into smaller crates can often lead to much faster compiles.
All compilers have compilation units, there's not actually much interesting about Rust here other than using the word "crate" as a friendlier term for "compilation unit".
What you may be referring to instead is Cargo's decision to re-use the notion of a crate as the unit of package distribution. I don't think this was necessarily a bad idea (it certainly made things simpler, which matters when you're bootstrapping an ecosystem), but it's true that prevailing best practices since then have led to Rust's ecosystem having comparatively larger compilation units (which itself isn't necessarily a bad thing either; larger compilation units do tend to produce faster code). I would personally like to see Cargo provide a way to decouple the unit of distribution from the unit of compilation, which would give us free parallelism (which currently today rustc needs to tease out via parallel codegen units (and the forthcoming parallel frontend)) and also assuage some of the perpetual hand-wringing about how many crates are in a dependency tree (which is exactly the wrong measure as getting upset about how many source files are in your C program). This would be a fully backwards-compatible change.
> would have ever been considered for omission, regardless of their compile-time cost, because that would have compromised Rust's other goals.
That basically says compiler speed isn’t a goal at all for Rust. I think that’s not completely true, but yes, speed of generated code definitely ranks very high for rust.
In contrast, Wirth definitely had the speed at which the Oberon compiler compiled code as a goal (often quoted as that he only added compiler optimizations if they made the compiler itself so much faster that it didn’t become slower because of the added complexity, but I’m not sure he was that strict)
“It is hardly surprising that certain measures for code improvement may yield considerable gains with modest effort, whereas others may require large increases in compiler complexity and size while yielding only moderate code improvements, simply because they apply in rare cases only.
Indeed, there are tremendous differences in the ratio of effort to gain. Before the compiler designer decides to incorporate sophisticated optimization facilities, or before deciding to purchase a highly optimizing, slow and expensive compiler, it is worth while clarifying this ratio, and whether the promised improvements are truly needed.
Furthermore, we must distinguish between optimizations whose effects could also be obtained by a more appropriate formulation of the source program, and those where this is impossible.
The first kind of optimization mainly serves the untalented or sloppy programmer, but merely burdens all the other users through the increased size and decreased speed of the compiler.
As an extreme example, consider the case of a compiler which eliminates a multiplication if one factor has the value 1. The situation is completely different for the computation of the address of an array element, where the index must be multiplied by the size of the elements. Here, the case of a size equal to 1 is frequent, and the multiplication cannot be eliminated by a clever trick in the source program.”
> That basically says compiler speed isn’t a goal at all for Rust
No, it says that language design inherently involves difficult trade-offs, and the Rust developers consciously decided that some trade-offs were worth the cost. And their judgement appears to have been correct, because Rust today is more successful than even the most optimistic proponent would have dared to believe in 2014; that users are asking for something implies that you have succeeded to the point of having users at all, which is a good problem to have and one that nearly no language ever enjoys.
In the context of Oberon, let's also keep in mind that Rust is a bootstrapped compiler, and in the early days the Rust developers were by far the most extensive users of the language; nobody on Earth was more acutely affected by compiler performance than they were. They still chose to prefer runtime performance (to be competitive with C++) over compiler performance (to be competitive with Go), and IMO they chose correctly.
And as for the case of Oberon, its obscurity further confirms that prioritizing compiler performance at all cost is not a royal road to popularity.
A few points, Wirth usually always bootstraped his compilers, writing just enough Assembly for the minimal version 0 of the language and go from there.
Secondly, as we all know it isn't grammar and semantics alone that sell compilers, it is someone heavily pushing for adoption, deep pockets companies willing to do everything for adoption, or some successful product on the market that makes learning the language unavoidable when reaching for said product.
Oberon had nothing of that.
C++ had being from UNIX's birthplace, quick adoption by C compiler vendors and UNIX clones, exactly because of that. While being pushed by IBM, Microsoft and Apple on their GUI OS frameworks.
Rust as great language as it is, if it had been some university language done as someone thesis, not tied to Mozzilla, and their efforts to fix Firefox security issues with their C++ codebase, most likely would not taken off.
This was a big reason for dart canceling its previous macros attempt (as I understand it). Fast compilation is integral for Flutter development - which accounts for a late percentage of dart usage - so after IIRC more than two years of developing it they still ended up not going through with that iteration of macros because it would make hot reload too slow. That degree of level-headedness and consideration is worthy of respect IMO.
One of the issue why compile times are so awful is that all dependencies must be compiled for each project.
20 different projects use the same dependency? They each need to recompile it.
This is an effect of the language not having a proper ABI for compiling libraries as dynamically loadable modules, which in itself presents many other issues, including making distribution of software a complete nightmare.
> This is an effect of the language not having a proper ABI for compiling libraries as dynamically loadable modules
No, this is a design decision of Cargo to default to using project-local cached artifacts rather than caching them at the user or system level. You can configure Cargo to do so if you'd like. The reason it doesn't do this by default is because Cargo gives crates great latitude to configure themselves via compile-time flags, and any difference in flags means you get a different compiled artifact anyway. On top of that, there's the question of what `cargo clean` should do when you have a global cache rather than a local one.
Why can't Cargo have a system like PyPI where library author uploads compiled binary (even with their specific flags) for each rust version/platform combination, and if said binary is missing for certain combination, fallback to local compile? Imagine `cargo publish` handle the compile+upload task, and crates.io be changed to also host binaries.
> Why can't Cargo have a system like PyPI where library author uploads compiled binary
Unless you have perfect reproducible builds, this is a security nightmare. Source code can be reviewed (and there are even projects to share databases of already reviewed Rust crates; IIRC, both Mozilla and Google have public repositories with their lists), but it's much harder to review a binary, unless you can reproducibly recreate it from the corresponding source code.
I don’t think it’s that much of a security nightmare: the basic trust assumption that people make about the packaging ecosystem (that they trust their upstreams) remains the same whether they pull source or binaries.
I think the bigger issues are probably stability and size: no stable ABI combined with Rust’s current release cadence means that every package would essentially need to be rebuilt every six weeks. That’s a lot of churn and a lot of extra index space.
> remains the same whether they pull source or binaries.
I don't think that's exactly true, it's definitely _easier_ to sneak something into a binary without people noticing than it is to sneak it into rust source, but there hasn't been an underhanded rust competition for a while so I guess it's hard to be objective about that.
Pretty much nobody does those two things at the same time:
- pulling dependencies with cargo
- auditing the source code of the dependencies they're building
You are either censoring and vetting everything or you're using dependencies from crates.io (ideally after you've done your due diligence on the crate), but should crates.io be compromised and inject malware in the crates' payload, I'm ready to bet nobody would notice for a long time.
I fully agree with GP that binary vs source code wouldn't change anything in practice.
> Pretty much nobody does those two things at the same time:
- pulling dependencies with cargo - auditing the source code of the dependencies they're building
Your “pretty much” is probably weaseling you out of any criticism here, but I fully disagree:
My IDE (rustrover) has “follow symbol” support, like every other IDE out there, and I regularly drill into code I’m calling in external crates. Like, just as often as my own code. I can’t imagine any other way of working: it’s important to read code you’re calling to understand it, regardless of whether it’s code made by someone else in the company, or someone else in the world.
My IDE’s search function shows all code from all crates in my dependencies. With everything equal regardless of whether it’s in my repo or not. It just subtly shades the external dependencies a slightly different color. I regularly look at a trait I need from another crate, and find implementations across my workspace and dependencies, including other crates and impls within the defining crate. Yes, this info is available on docs.rs but it’s 1000x easier to stay within my IDE, and the code itself is available right there inline, which is way more valuable than docs alone.
I think it’s insane to not read code you depend on.
Does this mean I’m “vetting” all the code I depend on? Of course not. But I’m regularly reading large chunks of it. And I suspect a large chunk of people work the way I do; There are a lot of eyeballs on public crates due to them being distributed as source, and this absolutely has a tangible impact on supply chain attacks.
> Does this mean I’m “vetting” all the code I depend on? Of course not.
Inspecting public facing parts of the code is one thing, finding nasty stuff obfuscated in a macro definition or in a Default or Debug implementation of a private type that nobody is ever going to check outside of auditors is a totally different thing.
> My IDE (rustrover) has “follow symbol” support
I don't know exactly how it works for RustRover, since I know Jetbrain has reimplemented some stuff on their own, but if it evaluates proc macros (like rust-analyzer) does, then by the time you step into the code it's too late, proc macros aren't sandboxed in any ways and your computer could be compromised already.
The point of my argument is not to say I’m vetting anything, but to say that there are tons of eyeballs on crates today, because of the fact that they are distributed as source and not a binary. It’s not a silver bullet but every little bit helps, every additional eyeball makes hiding things harder.
The original claim is that “pretty much no one” reads any of their dependencies, in order to support a claim that they should be distributed as binaries, meaning “if there was no source available at all in your IDE, it wouldn’t make a difference”, which is just a flatly wrong claim IMO.
A disagreement may be arising here about the definition of “audit” vs “reading” source code, but I’d argue it doesn’t matter for my point, which is that additional eyeballs matter for finding issues in dependencies, and seeing the source of your crates instead of a binary blob is essential for this.
> The original claim is that “pretty much no one” reads any of their dependencies,
No the claim is that very few people read the dependencies[1] enough to catch a malicious piece of code. And I stand by it. “Many eyeballs” is a much weaker guarantee when people are just doing “go to definition” from their code (for instance you're never gonna land on a build.rs file this way, yet they are likely the most critical piece of code when it comes to supply chain security).
[1] (on their machines, that is if you do that on github it doesn't count since you have no way to tell it's the same code)
> No the claim is that very few people read the dependencies[1] enough to catch a malicious piece of code.
You’re shifting around between reading enough to catch any issue (which I could easily do if a vulnerability was right there staring at me when I follow symbol) to catching all issues (like your comment about build.rs.) Please stick with one and avoid moving goal posts around.
There exists a category of dependency issues that I could easily spot in my everyday reading of my dependencies’ source code. It’s not all of them. Your claim is that I would spot zero of them, which is overly broad.
You’re also trying to turn this into a black-or-white issue, as if to say that if it isn’t perfect (ie. I don’t regularly look at build.rs), it isn’t worth anything, which is antithetical to good security. The more eyeballs the better, and the more opportunities to spot something awry, the better.
I'm not moving the goal post, a supply chain attack is an adversarial situation it is not about spotting an issue occurring at random, it is about spotting an issue specially crafted to avoid detection. So in practice you are either able to spot every kind of issues, or none of the relevant ones because if there's one kind that reliably slips through, then you can be certain that the attacker will focus on this kind and ignore the trivial to spot ones.
If anything, having access to the source code gives you an illusion of security, which is probably the worse place to be in.
The worse ecosystem when it comes to supply chain attacks is arguably the npm one, yet there anyone can see the source and there are almost two orders of magnitude more eyeballs.
In such an environment I’m doomed anyway, even if I’m vetting code. I don’t understand why the goal has to be “the ability to spot attacks specifically designed to prevent you from detecting.” For what you’re describing, there seems to be no hope at all.
It’s like if someone says “don’t pipe curl into bash to install software”, ok that may or may not be good advice. But then someone else says “yeah, I download the script first and give it a cursory glance to see what it’s doing”, wouldn’t you agree they’re marginally better off than the people who just do it blindly?
If not, maybe we just aren’t coming from any mutual shared experience. It seems flatly obvious to me that being able to read the code I’m running puts me in a better spot. Maybe we just fundamentally disagree.
> It’s like if someone says “don’t pipe curl into bash to install software”, ok that may or may not be good advice. But then someone else says “yeah, I download the script first and give it a cursory glance to see what it’s doing”, wouldn’t you agree they’re marginally better off than the people who just do it blindly?
I don't agree with your comparison, in this case it's more like downloading, then running it without having read it and then every once in a while look at a snippet containing a feature that interest you.
The comparison to “download the script and read it before you run it” would be to download the crate's repo, read it and then vendor the code you've read to use as a dependency, which is what I'd consider proper vetting (in this case the attacker would need to be much more sophisticated to avoid detection, it's still possible but in this case at least you've actually gained something), but it's a lot more work.
If you have reproducible builds it's no different. Without those binaries are a nightmare in that you can't easily link a given binary back to a given source snapshot. Deciding to trust my upstream is all well and good but if it's literally impossible to audit them that's not a good situation to be in.
I think it’s already probably a mistake to think that a source distribution consistently references a unique upstream source repository state; I don't believe the crate distribution layout guarantees this.
(I agree that source is easier to review and establish trust in; the observation is that once you read the upstream source you’re in the same state regarding distributors, since build and source distributions both modify the source layout.)
It might as well. If there is no definition of an ABI, nobody is going to build the tooling and infrastructure to detect ABI compatibility between releases and leverage that for the off-chance that e.g. 2 out of 10 successive Rust releases are ABI compatible.
You can have binary dependencies with a stable ABI; they're called C-compatible shared libs, provided by your system package manager. And Cargo can host *-sys packages that define Rust bindings to these shared libs. Yes, you give up on memory safety across modules, but that's what things like the WASM Components proposals are for. It's a whole other issue that has very little to do with ensuring safety within a single build.
docs.rs is just barely viable because it only has to build crates once (for one set of features, one target platform etc.).
What you propose would 1) have to build each create for at least the 8 Tier 1 targets, if not also the 91 Tier 2 targets. That would be either 8 or 99 binaries already.
Then consider that it's difficult to anticipate which feature combinations a user will need. For example, the tokio crate has 14 features [1]. Any combination of 14 different features gives 2^14 = 16384 possible configurations that would all need to be built. Now to be fair, these feature choices are not completely independent, e.g. the "full" feature selects a bunch of other features. Taking these options out, I'm guessing that we will end up with (ballpark) 5000 reasonable configurations. Multiply that by the number of build targets, and we will need to build either 40000 (Tier 1 only) or 495000 binaries for just this one crate.
Now consider on top that the interface of dependency crates can change between versions, so the tokio crate would either have to pin exact dependency versions (which would be DLL hell and therefore version locking is not commonly used for Rust libraries) or otherwise we need to build the tokio crate separately for each dependency version change that is ABI-incompatible somewhere. But even without that, storing tens of thousands of compiled variants is very clearly untenable.
Rust has very clearly chosen the path of "pay only for what you use", which is why all these library features exist in the first place. But because they do, offering prebuilt artifacts is not viable at scale.
You could get a lot of benefit from a much smaller subset. For example, just the "syn" crate with all features enabled on tier 1 targets (so ~8 builds total) would probably save a decent chunk off almost everybody's build.
It runs counter to Cargos curreat model where the top-level workspace has complete control over compilation, including dependencies and compiler flags. I've been floating an idea of "opaque dependencies" that are like python depending on C libraries or a C++ library dependening on a dynamic library.
That would work for debug builds (and that's something that I would appreciate) but not for release, as most of the time you want to compile for the exact CPU you're targeting not just for say “x86 Linux” to make sure your code is optimized properly using SIMD instructions.
At some point, the community is also responsible for the demanding expectation of a "not slow" compiler.
What's "slow"? What's "fast"? It depends. It depends on the program, the programmer, his or her hardware, the day of the week, the hour of the day, the season, what he or she had for lunch, ...
It's a never ending quest.
I, for exemple, am perfectly happy with the current benchmark of the rust compiler. I find a x2 improvement absolutly excellent.
You're conflating language design and compiler architecture. It's hard to increment on a compiler to get massive performance improvement, and rearchitecture can help, but you don't necessarily need to change anything to the language itself in that regard.
Roslyn (C#) is the best example of that.
It's a massive endeavor and would need significant fundings to happen though.
Language design can have massive impact on compiler architecture. A language with strict define-before-use and DAG modules has the potential to blow every major compiler out of the water in terms of compile times. ASTs, type checking, code generation, optimization passes, IR design, linking can all be significantly impacted by this language design choice.
No, language design decisions absolutely have a massive impact the performance envelope of compilers. Think about things like tokenization rules (Zig is designed such that every line can be tokenized independently, for example), ambiguous grammars (most vexing parse, lexer hack etc.), symbol resolution (e.g. explicit imports as in Python, Java or Rust versus "just dump eeet" imports as in C#, and also things whether symbols can be defined after being referenced) and that's before we get to the really big one: type solving.
This kind of comment is funny because it reveals how uninformed people can be while having a strong opinion on a topic.
Yes grammar can impact how theoretically fast a compiler can be, and yes the type system ads more or less works depending on how it's designed, but none of these are what makes Rust compiler slow. Parsing and lexing are negligible fraction of compile time, and typing isn't particularly heavy in most cases (with the exception of niches crates who abuse the Turing completeness of the Trait system). You're not going to make big gains by changing these.
The massive gains are to be made later in the pipeline (or earlier, by having a way to avoid re-compiling pro macros and their dependencies before the actual compilation can even start).
The point was language design influences compiler performance. Rust is heavily designed around "zero-cost abstraction", ie. generate tons of IR and let the backend sort it out. Spending all the time in LLVM passes is what you would expect from this.
Had you read the linked blog post, you'd have seen that here this isn't so much an issue with LLVM having too much work, but rustc being currently unable to split the work into parallelizable chunks before sending it to LLVM, and as such it takes a very long time not because LLVM has too much things to do, but because it does it in a single-threaded fashion, leaving tons of performance on the table.
> Rust is heavily designed around "zero-cost abstraction", ie. generate tons of IR and let the backend sort it out.
Those two aren't equivalent: Rust is indeed designed around zero-cost abstraction, and it currently generates tons of IR for the backend to optimize, but it doesn't have to, it could run some optimizations in the front-end so it generates less IR. In fact there has been ongoing work to do exactly this to improve compiler performance. But this required rearchitecturing the compiler in depth (IIRC Rust's MIR has been introduced for that very reason).
The lexer hack is a C thing, and Ive rarely heard anyone complain about C compiler performance. That seems more like an argument that the grammar doesn't have that much of an impact on compiler performance as other things.
Yeah. It's exactly backwards, because good language design doesn't make anything except parsing faster. The problem is that some languages have hideously awful grammars that make things slower than they ought to be.
The preprocessor approach also generates a lot of source code that then needs to be parsed over and over again. The solution to that isn't language redesign, it's to stop using preprocessors.
The preprocessor does not necessarily create a lot of source code in C. I can, if you expand arguments multiple times and there is an exponential explosion for nested macros, but this also easy to avoid.
The key to unlocking a 10x improvement to compilation speeds will like be
multithreading. I vaguely remember that LLVM struggled with this and I am not sure where it stands today. On the frontend side language (not compiler) design will
affect how well things can be parallelized, e.g. forward declatations probably help, mandatory interprocedural anaylyses probably hurt.
Having said that, we are in a bad shape when golang compiling 40kLOC in 2s is
a celebrated achievement.
Assuming this is single threaded on a 2GHz machine, we
2s * 2GHz / 40kLOC = 100k [cycles] / LOC
That seems like a lot of compute and I do not see how this cannot be improved substantially.
Shameless plug: the Cwerg language (http://cwerg.org) is very focussed on compilation speeds.
maybe rustc will never be re-architectured (although it has already been rewritten once), but with developing rust standard there will come new Rust implementations. And there is a chance that they will prioritize performance when architecting.
The root cause of the problem is not the compiler. It's the language. If you compile a C program that links to a lot of libraries, the compiler compiles just the source code you wrote, and then the linker combines that with the pre-compiled libraries. Linking is a comparatively fast operation, so the total time is roughly what it took to compile just your source. Rust has chosen a design that requires it to compile not just your source, but also a fair chunk of the the libraries source code as well. Those libraries are usually many times the size of your source code.
Unsaid here is modern C++ programs suffer from the same problem, because modern C++ libraries are mostly .h files that are stuffed full of templates. The outcome is the same as Rust: slow compilation, even with a GNU C++ compiler.
The outcome is the same because Rust's generics have a lot in common with C++ templates. Both a generic Rust function and a C++ template are a kind of type safe macro. Like all macro's they generate source code, customised by the parameters the caller supplied, and because its customised that source code can't be pre-compiled and put into a library.
The downside of this is not just slow compile times. It's also means very fat binaries. And it means security issues in a library can't be fixed by shipping a new version of a dll, you have to recompile the original program.
> Re-architecting the rust compiler to be faster is probably not going to happen.
This is a statement without the weight of evidence. The Rust compiler has been continually rearchitected since 1.0, and has doubled its effective performance multiple times since then.
> The second way forward is performing massive changes and/or refactorings to the implementation of the compiler. However, that is of course challenging, for a number of reasons.
> if you change one thing at the “bottom” layer of the compiler, you will then have to go through hundreds of places and fix them up, and also potentially fix many test cases, which can be very time-consuming
> You can try to perform the modifications outside the main compiler tree, but that is almost doomed to fail. Or, you will need to do the migration incrementally, which might require maintaining two separate implementations of the same thing for a long time, which can be exhausting.
> this is a process that takes several years to finish. That’s the scale that we’re dealing with if we would like to perform some massive refactoring of the compiler internals
There are other easier paths to improving the build times for common developer workflows.
Rust can likely never be rearchitected without causing a disastrous schism in the community, so it seems probable that compilation will always be slow.