rustc might never be as fast as the Go compiler because the language has so many additional features, but it always makes me so excited to see the continued progress in making the compiler ever faster.
Thank you for the hard work here!
Btw, mentioned in the article are these tools: “ All of the above improvements (and most of the ones in my previous posts) I found by profiling with Cachegrind, Callgrind, DHAT, and counts, but there are plenty of other profilers out there.” Does anyone have any good resources on using these with Rust, or just in general with C or C++?
I think rustc will never be faster than go or even java.
Go has made quite a few language concessions to be fast to compile and is designed with that in mind.
And it really shows, using auto reloading in Go feels like using Ruby or Python which is just great.
From my usage standpoint Go has basically no compile time at all.
And awful compile times can be a huge hindrance to productivity.
I used to do web dev in Scala, but waiting for the sleepy compiler is one of the reasons is switched to Go.
Scala is a nice language but the long compile times in combination with the vm/jetty cycle feels quite a bit slower than Rust.
> And awful compile times can be a huge hindrance to productivity. I used to do web dev in Scala, but waiting for the sleepy compiler is one of the reasons is switched to Go.
If the compiler catches bugs that would otherwise only be found at run time, then the additional compile time pays for itself many times over in terms of productivity.
I think he is referring to situation when compiler both catches errors at compile-time and is fast. After all the validation part happening in the fronted is rarely the most resource consuming thing that compiler does.
And I think rapsey is referring to the fact that a compiler with a more powerful type system allows you to encode more logic in your types. This, in turn, means it will catch more errors at compile time at the cost of longer compile times.
Yes, but I think the OP referring to Go as he dont want one or the other. He Wants both! And Go being near no-compiling time meant that best of both world ( From his perspective )
Well it can run time during testing and run time in production. From my experience in Java I have caught quite a few bug at run time in testing. And it is not that bad.
Any advanced type system, whether F#, C++, Haskell, Scala, Rust, TypeScript...is inevitably going to have a significantly slower compiler than a more basic system (Go, Java, C).
That's just part of the tradeoff; though obviously you can optimize within those bounds.
It's about as universal as the runtime "rule": compiled perf > interpreted perf. Not technically inviolable, but practically so.
---
P.S. And while the F# compiler is indeed faster than rustc, it's not apples-to-apples. F# compiles to CLR; Rust compiles through LLVM all the way to native. If Rust deferred work to runtime with an LLVM interpreter (http://llvm.org/docs/CommandGuide/lli.html ?), it would improve compile-time performance, at the cost of runtime overhead.
I don't think this is true. C compilers often have a whole bunch of optimizations built in, which could easily make them quite a lot slower than a less-optimized compiler for a language with a more advanced/complex type system. There are other factors as well, such as the efficiency of the compiler--the same C compiler implemented in C and compiled with an advanced C compiler will handily beat a C compiler written in Python and executed on CPython. But to your point, all-else-equal, a program in a language with an advanced type system takes longer to compile than a simpler type system.
I mention this at the end of my comment, but yes the second very significant factor is the nature of transformation.
A SASS (CSS preprocessor language) compiler can be very fast because it does very little.
Native compilers must do significantly more transformation than other compilers, e.g. F# CLR compiler. This is doubly so if you request optimized output (but that's probably not the case in this discussion).
F# compiles to native code via Mono AOT, NGEN (available since .NET 1.0 which many seem to forget), .NET Native (does require a hack and some care due to missing support for some MSIL opcodes), Unity's IL2CPP.
Naturally Rust could offer an interpreter of some sort, however it still isn't there today, so we got to use what is available.
I'm not 100% sure blaming LLVM is the way forward, as Jonathan Blow and his Jai language are able to compile and link a full 3D game in under a second using it.
EDIT: Although I've now verified that Jai no longer uses LLVM for debug builds, when it did, it would have been able to do a compilation of the same game within only a few seconds as well, so my post isn't _terribly_ incorrect, thankfully =b
Possibly not, but there does seem to be strong correlation between languages using LLVM and long compile times. Perhaps Jai is an exception, but it's hard to know how or why that is given that he has not released his language.
AFAIK jai only uses LLVM in release mode since it does slow down the build. This makes sense since if you have optimizations off, you only have to do codegen. If you are using LLVM for codegen, then you have to codegen twice! Once for LLVM IR and once for machine code.
Even when not using LLVM, Jai still does "codegen twice." Most compilers today (even JITs) have at least one IR in between the syntax tree and the generated code.
The existence of LLVM IR is not the problem- rather, it's how it gets used (on both sides of the API). Generating a lot of naive IR and letting the optimizer clean it up, for example, has a large cost.
And, while I'm not too up to date on the details of Jai, the last thing I heard it was still very fast to compile even in release builds that did use LLVM. That is, the Jai compiler is smarter about how it generates IR.
This is basically just saying that LLVM has an IR. While going straight from AST to machine code may sound like a great thing for compilation speed, it makes compiler maintenance really tough. Many AOT-based compilers nowadays are converging on four levels of IR, which seems to be a sweet spot. (Swift has AST, SIL, LLVM IR, MachineInstr; Rust has AST, MIR, LLVM IR, MachineInstr; GCC has AST, GENERIC, GIMPLE, RTL.)
The thing Pascal-family languages have going for them (and I'm including Go here, because there is a lot of Pascal-family influence in Go, although it's not immediately obvious) is first and foremost the way they resolve dependencies. A detailed explanation of this can be found in the talk "Go at Google" (https://talks.golang.org/2012/splash.article#TOC_5.). I'm not sure about Rust, but maybe while trying to ensure a high degree of interoperability with C/C++ they also "inherited" some of their dependency management issues?
Sorry for offending you by mentioning Go, I was referring to the two parent comments who mentioned it. I'm not a good judge of language complexity (are you?), but honestly I don't think that the Go compiler is much less complex than Delphi's - the "Delphi language" hasn't evolved much in 20 years (which is not necessarily a bad thing!). Ok, they now have cough generics, and - yay! - closures, but that's about it...
AdaCore just built an Ada front end for LLVM, so maybe not for long :-).
I'm not sure I understand the problem with rust build times. In Ada, at least with GNAT, a full rebuild of 200KLOC can be a bit long, depending on your use of generics, and your number of cores (thanks to AMD, build times will soon be ridiculously short). But next builds with slight modifications are quite fast, thanks to modular compilation (every module built independently, same as for C, thanks to spec/body separation if you just change the body of a module you just recompile the specific module) and incremental compilation (only rebuild what changed and their dependencies).
Is there something inherently slow in the Rust compiler that disallows those ?
I know that writing an Ada compiler that could compile units independently seemed impossible to everyone at first, until (the late, sadly) Robert Dewar worked it out with RMS : https://news.ycombinator.com/item?id=15880160
I don't know Rust enough, just that what they're doing is amazing, and I hope they're not too focused on the small scale optimizations (which are great!) and look at the high level optimizations too, and especially to what's been done elsewhere through sweat and pain.
Well, there are other Ada compilers around besides GNAT, although GNAT is the only affordable one to most mortals.
Most people might also not be aware that Rational Software started their business by selling Ada machines, where one could enjoy an experience somehow similar to Lisp Machines, just with Ada instead.
None of those do the same kind of static analysis Rust is doing.
Also F# doesn't compile fast either, in part because it is written in F# with functional idioms that are sometimes pokey.
Eventually the Rust compiler will be fully incremental, possibly tied into the debugger so it can patch running code with just the diff, retaining state while doing so, but this is probably 5 years out.
> And awful compile times can be a huge hindrance to productivity
Agree a lot with that. I write quite a bit of code in Rust and I use the JetBrains CLion IDE with the IdeaVim and Rust plugins.
CLion is very helpful when working with Rust code. It understands the language quite well and because of that it can help you by pointing out things that aren't going to work without having to constantly recompile your code.
For students and faculty members, JetBrains give out licenses free of charge that are valid for 1 year, and which can be renewed while you are still a student or faculty member. https://www.jetbrains.com/student/
Disclaimer: I am under the impression that the following is exploratory work and so this ought to be taken with a grain of salt until it is communicated through official channels.
Main idea: Essentially, compile only what has changed, re-use compiled forms of everything that hasn't.
Essentially, pre-compiled dependencies will be sourced and plugged in to a Rust project. This presents great security concerns, but also great opportunities to advance software development. A large system has a vast network of crate dependencies. Certain versions of these crates will be pre-compiled, each uniquely identifiable through a hash. Some of these crates will even be audited and potentially certified. Hundreds of black box, pre-compiled crates will each be sandboxed in a very secure fashion such that it does only as specified and no more. No sandboxed library can reach beyond its advertised behavior. Unchanged, locally-developed parts can also be compiled and used through this flow as well. WASM projects are facilitating much of this work.
I may be missing important parts from this explanation, so hopefully it is corrected by someone more knowledgeable.
> Essentially, compile only what has changed, re-use compiled forms of everything that hasn't.
That's what incremental compiling does, and it has been enabled for a while. Unfortunately, it doesn't help on fresh builds.
> Essentially, pre-compiled dependencies will be sourced and plugged in to a Rust project.
There are two aspects here. The more recent one, which you are thinking of, is building and shipping procedural macros as WASM binaries. Procedural macros are very powerful, but they take a token stream as input to allow for future changes to the language. Because of this, basically every macro uses a Rust parser crate called syn. Since it's a fully-fledged parser, syn takes a while to compile (30 seconds or so, certainly not minutes), which many people find annoying, and it gets worse if you end up using different versions of syn for different proc macro crates. The plan here is to build the proc macros (including syn) somewhere on the Rust infrastructure and ship the binaries to the users. The sandboxing story is somewhat complicated: proc macros and build scripts can legitimately read or download files, or produce non-deterministic output in other ways. A WASM macro would not be able to do this, so the whole thing would be opt-in. It also provides no benefit for crates that don't use procedural macros. See https://github.com/dtolnay/watt for more details.
The other possible avenue is MIR-only rlibs. When you compile a dependency, you get machine code (with the exception of generic code). It might be possible to compile crates to MIR (again, on the Rust infrastructure) and only do the final codegen on the user's computer. But that's still complex, and not necessarily much faster. See https://github.com/rust-lang/rust/issues/38913.
> rustc might never be as fast as the Go compiler because the language has so many additional features
Not necessarily true. The d compiler runs incredibly fast, for compiling the type of code you'd write in go; and it only slows down if you use a lot of complicated features like templates or CTFE.
Thank you for the hard work here!
Btw, mentioned in the article are these tools: “ All of the above improvements (and most of the ones in my previous posts) I found by profiling with Cachegrind, Callgrind, DHAT, and counts, but there are plenty of other profilers out there.” Does anyone have any good resources on using these with Rust, or just in general with C or C++?