6 ms·
The more your compiler does for you at build time, the longer it will take to build, it's that simple. Go has sub-second build times even on massive code-bases
by lordofgibbons 1y ago
The more your compiler does for you at build time, the longer it will take to build, it's that simple.
Go has sub-second build times even on massive code-bases. Why? because it doesn't do a lot at build time. It has a simple module system, (relatively) simple type system, and leaves a whole bunch of stuff be handled by the GC at runtime. It's great for its intended use case.
When you have things like macros, advanced type systems, and want robustness guarantees at build time.. then you have to pay for that.
- ChadNauseam 1y agoThat the type system is responsible for rust's slow builds is a common and enduring myth. `cargo check` (which just does typechecking) is actually usually pretty fast. Most of the build time is spent in the code generation phase. Some macros do cause problems as you mention, since the code that contains the macro must be compiled before the code that uses it, so they reduce parallelism.
- rstuart4133 1y ago> Most of the build time is spent in the code generation phase. I can believe that, but even so it's caused by the type system monomorphising everything. When it use qsort from libc, you are using per-compiled code from a library. When you use slice::sort(), you get custom assembler compiled to suit your application. Thus, there is a lot more code generation going on, and that is caused by the tradeoffs they've made with the type system. Rusts approach give you all sorts of advantages, like fast code and strong compile time type checking. But it comes with warts too, like fat binaries, and a bug in slice::sort() can't be fixed by just shipping of the std dynamic library, because there is no such library. It's been recompiled, just for you. FWIW, modern C++ (like boost) that places everything in templates in .h files suffers from the same problem. If Swift suffers from it too, I'd wager it's the same cause.
- badmintonbaseba 1y agoIt's partly by the type system. You can implement a std::sort (or slice::sort()) that just delegates to qsort or a qsort-like implementation and have roughly the same compile time performance as just using qsort straight. But not having to is a win, as the monomorphised sorts are just much faster at runtime than having to do an indirect call for each comparison.
- estebank 1y agoThis is a pattern a crate author can rely on (write a function that uses genetics that immediately delegates to a function that uses trait objects or converts to the needed types eagerly so the common logic gets compiled only once), and there have been multiple efforts to have the compiler do that automatically. It has been called polymorphization and it comes up every now and then: https://internals.rust-lang.org/t/add-back-polymorphization/22879 https://internals.rust-lang.org/t/add-back-polymorphization/...
- rstuart4133 1y agoAll true, but given the number of "Rust compile time is slow" posts that blame the compiler I think it's safe to say most programmers don't understand the real underlying trade-off that's causes it. Not all programmers of course - if you look at std there are many places that split types into generic and non-generic parts so the compiler will reuse as much code as possible, but it does come at the cost of additional complexity. Worse if you aren't already aware of why they are doing it, the language does a marvellous job of hiding the reason that complexity is there. I'd wager a novice Rust programmer is as befuddled by it as a JavaScript programmer coming across his first free() call in C. I have this dream of a language like Rust that makes the trade-off plain, so the programmer is always aware of "this is a zero cost abstraction - you're just making it plain via the type system your doing the right thing" and "I'm going to have to generate a lot of code for this". Then go a step further and put the types and source you want to export to other libraries in a special elf section in the .so so you don't need the source to link against it, then go another step further and make the programmer using the .so explicitly instantiate any stuff that does require a lot of code generated so he is aware of what is happening. That said, I don't think it would help the compile time problem in most cases. C++ already does something close by forcing you to put exported stuff in .h files, and they ended up with huge .h files and slow compiles anyway. Nevertheless doing that would make for a Rust like language, that, unlike Rust, supported an eco-system of precompiled libraries just like C does. Rust is so wedded to transparent monomorphisation it looks near impossible now.
- tedunangst 1y agoI just ran cargo check on nushell, and it took a minute and a half. I didn't time how long it took to compile, maybe five minutes earlier today? So I would call it faster, but still not fast. I was all excited to conduct the "cargo check; mrustc; cc" is 100x faster experiment, but I think at best, the multiple is going to be pretty small.
- ChadNauseam 1y agoDid you do it from a clean build? In that case, it's actually a slightly misleading metric, since rust needs to actually compile macros in order to typecheck code that uses them. (And therefore must also compile all the code that the macro depends on.) My bad for suggesting it, haha. Incremental cargo check is often a better way of seeing how long typechecking takes, since usually you haven't modified any macros that will need to be recompiled. On my project at work, incremental cargo check takes `1.71s`.
- estebank 1y agoSide note: There's an effort to cache proc macro invocations so that they get executed only once if the item they annotate hasn't changed: https://github.com/rust-lang/rust/pull/129102 https://github.com/rust-lang/rust/pull/129102 There are multiple caveats on providing this to users (we can't assume that macro invocations are idempotent, so the new behavior would have to be opt in, and this only benefits incremental compilation), but it's in our radar.
- pjmlp 1y agoThat is something I never have to care on my C++ projects, because I always make use of binary libraries, unless I am forced to compile from source. Unfortunately that doesn't seem to ever be a scenario cargo will support out of the box.
- ChadNauseam 1y agoThe actual reason that you don't have to care about this on your C++ project is because C++ doesn't let you define macros in C++, you can only define them in the preprocessor language. Therefore no compilation is needed to execute them.
- deleted 1y ago[deleted]
- duped 1y agoI think this is mostly a myth. If you look at Rust compiler benchmarks, while typechecking isn't _free_ it's also not the bottleneck. A big reason that amalgamation builds of C and C++ can absolutely fly is because they aren't reparsing headers and generating exactly one object file so the linker has no work to do. Once you add static linking to the toolchain (in all of its forms) things get really fucking slow. Codegen is also a problem. Rust tends to generate a lot more code than C or C++, so while the compiler is done doing most of its typechecking work, the backend and assembler has a lot of things to chuck through.
- treyd 1y agoNot only does it generate more code, the initially generated code before optimizations is also often worse. For example, heavy use of iterators means a ton of generics being instantiated and a ton of call code for setting up and tearing down call frames. This gets heavily inlined and flattened out, so in the end it's extremely well-optimized, but it's a lot of work for the compiler. Writing it all out classically with for loops and ifs is possible, but it's harder to read.
- estebank 1y agoFor loops are sugar around an Iterator instantiation: for i in 0..10 {} translates to roughly let mut iter = Range { start: 0, end: 10 }.into_iter(); while let Some(i) = iter.next() {}
- fingerlocks 1y agoThe swift compiler is definitely bottle necked by type checking. For example, as a language requirement, generic types are left more or less in-tact after compilation. They are type checked independent of what is happening. This is unlike C++ templates which are effectively copy-pasting the resolved type with the generic for every occurrence of type resolution. This has tradeoffs: increased ABI stability at the cost of longer compile times.
- willtemperley 1y ago
- cogman10 1y agoYes but I'd also add that Go specifically does not optimize well. The compiler is optimized for compilation speed, not runtime performance. Generally speaking, it does well enough. Especially because it's usecase is often applications where "good enough" is good enough (IE, IO heavy applications). You can see that with "gccgo". Slower to compile, faster to run.
- cherryteastain 1y agoIs gccgo really faster? Last time I looked it looked like it was abandoned (stuck at go 1.18, had no generics support) and was not really faster than the "actual" compiler.
- cogman10 1y agoDigging around, looks like it's workload dependent. For pure computational workloads, it'll be faster. However, anything with heavy allocation will suffer as apparently the gccgo GC and GC related optimizations aren't as good as cgo's.
- haiku2077 1y agoAs of about five years ago gccgo was slower for almost all workloads: https://meltware.com/2019/01/16/gccgo-benchmarks-2019.html https://meltware.com/2019/01/16/gccgo-benchmarks-2019.html
- pclmulqdq 1y agoGo defaults to an unoptimized build. If you want it to run heavy optimization passes, you can turn those on with flags. Rust defaults to doing most of those optimizations on every build and allows you to turn them off.
- Zardoz84 1y agoDlang compilers does more than any C++ compiler (metaprogramming, a better template system and compile time execution) and it's hugely faster. Language syntax design has a role here.
- Mawr 1y agoNot really. The root reason behind Go's fast compilation is that it was specifically designed to compile fast. The implementation details are just a natural consequence of that design decision. Since fast compilation was a goal, every part of the design was looked at through a rough "can this be a horrible bottleneck?", and discarded if so. For example, the import (package) system was designed to avoid the horrible, inefficient mess of C++. It's obvious that you never want to compile the same package more than once and that you need to support parallel package compilation. These may be blindingly obvious, but if you don't think about compilation speed at design time, you'll get this wrong and will never be able to fix it. As far as optimizations vs compile speed goes, it's just a simple case of diminishing returns. Since Rust has maximum possible perfomance as a goal, it's forced to go well into the diminishing returns territory, sacrificing a ton of compile speed for minor performance improvements. Go has far more modest performance goals, so it can get 80% of the possible performance for only 20% of the compile cost. Rust can't afford to relax its stance because it's competing with languages like C++, and to some extent C, that are willing to go to any length to squeeze out an extra 1% of perfomance.
- pjmlp 1y agoC++23 modules compile really fast now, moreso when helped with incremental compilation, incremental linking, and a culture that has not any qualms depending on binary libraries.
- jstanley 1y ago> Go has sub-second build times even on massive code-bases. Unless you use sqlite, in which case your build takes a million years.
- Groxx 1y agoYeah, I deal with multiple Go projects that take a couple minutes to link the final binary, much less build all the intermediates. Compilation speed depends on what you do with a language. "Fast" is not an absolute, and for most people it depends heavily on community habits. Rust habits tend to favor extreme optimizability and/or extreme compile-time guarantees, and that's obviously going to be slower than simpler code.
- infogulch 1y agoTry https://github.com/ncruces/go-sqlite3 https://github.com/ncruces/go-sqlite3 it runs sqlite in WASM with wazero, a pure Go WASM runtime, so it builds without any CGo required. Most of the benchmarks are within a few % of the performance of mattn/go-sqlite3.
- phplovesong 1y agoThats not really true. As a counter example, Ocaml has a very advanced type system, full typeinference, generics and all that jazz. Still its on par, or even faster to compile than Go.
- teleforce 1y ago>When you have things like macros, advanced type systems, and want robustness guarantees at build time.. then you have to pay for that. Go and Dlang compilers were designed by those that are really good at compiler design and that's why they're freaking fast. They designed the language around the compiler constraints and at the same managed to make the language intuitive to use. For examples, Dlang has no macro and no unnecessary symbols look-up for the ambiguous >>. Because of these design decisions both Go and Dlang are anomaly for fast compilation. Dlang in particular is notably more powerful and expressive compared to C++ and Rust even with its unique hybrid GC and non-GC compilation. In automotive industry it's considered a breakthrough and game changing achievement if you have a fast transmission for seamless auto and manual transmission such as found in the latest Koenigsegg hypercar [1]. In programming industry however, nobody seems to care. Walter Bright the designer of Dlang has background in mechanical engineering and it shows. [1] Engage Shift System: Koenigsegg new hybrid manual and automatic gearbox in CC850: https://www.topgear.com/car-news/supercars/heres-how-koenigseggs-new-manualautomatic-cc850-gearbox-works https://www.topgear.com/car-news/supercars/heres-how-koenigs...
- pjmlp 1y agoIt isn't an anomaly, it was pretty standard during the 1990's, until C and C++ took over all other compiled languages, followed by a whole generation educated in scripting languages.
- bjoli 1y agoChez scheme handles macros that expand to macros that expand to tens of thousands of lines of code in a breeze. Macros don't have to be slow.