5 ms·
It's certainly possible to think of language features that would preclude trivially-achievable high-performance compilation. None of those language features tha
by kibwen 1y ago
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.
- panstromek 1y agoThere 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.
- kibwen 1y agoThis 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.
- zozbot234 1y agoAIUI, "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.
- kibwen 1y agoIndeed, 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).
- zozbot234 1y agoWhy 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.
- kibwen 1y agoThe 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." https://github.com/rust-lang/rust/issues/48055#issuecomment-1837424794 https://github.com/rust-lang/rust/issues/48055#issuecomment-...
- WhyNotHugo 1y agoMacros themselves are a terrible hack to work around support for proper reflection. The entire Rust ecosystem would be reshaped in such fascinating ways if we had support for reflection. I'd love to see this happen one day.
- kibwen 1y ago> 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.
- adrian17 1y agoThey 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.
- MrJohz 1y agoIf 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.
- swsieber 1y agoWhat about crates as the unit of compilation? I am genuinely curious because it's not clear to me what trade-offs there are around that decision.
- pornel 1y agoIt'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.
- nicoburns 1y ago> 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.
- littlestymaar 1y agoYes the actual implementation is far from what could be, but the argument was that it's not a language design issue, but an implementation one.
- nicoburns 1y agoAgreed on that
- kibwen 1y agoAll 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.
- Someone 1y ago> 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) http://www.projectoberon.net/wirth/CompilerConstruction/CompilerConstruction.pdf http://www.projectoberon.net/wirth/CompilerConstruction/Comp..., section 16.1: “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.”
- kibwen 1y ago> 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.