upvote
I 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.

reply
The meme that static linking is slow or produces anything other than the best executables is demonstrably false and the result of surprisingly sinister agendas. Get out readelf and nm and PS sometime and do the arithematic: most programs don't link much of glibc (and its static link is broken by design, musl is better at just about everything). Matt Godbolt has a great talk about how dynamic linking actually works that should give anyone pause.

DLLs got their start when early windowing systems didn't quite fit on the workstations of the era in the late 80s / early 90s.

In about 4 minutes both Microsoft and GNU were like, "let me get this straight, it will never work on another system and I can silently change it whenever I want?" Debian went along because it gives distro maintainers degrees of freedom they like and don't bear the costs of.

Fast forward 30 years and Docker is too profitable a problem to fix by the simple expedient of calling a stable kernel ABI on anything, and don't even get me started on how penetrated everything but libressl and libsodium are. Protip: TLS is popular with the establishment because even Wireshark requires special settings and privileges for a user to see their own traffic, security patches my ass. eBPF is easier.

Dynamic linking moves control from users to vendors and governments at ruinous cost in performance, props up bloated industries like the cloud compute and Docker industrial complex, and should die in a fire.

Don't take my word for it, swing by cat-v.org sometimes and see what the authors of Unix have to say about it.

I'll save the rant about how rustc somehow manages to be slower than clang++ and clang-tidy combined for another day.

reply
CppCon 2018: Matt Godbolt “The Bits Between the Bits: How We Get to main()"

https://www.youtube.com/watch?v=dOfucXtyEsU

reply
I think you're confused about my comment and this thread - I'm talking about build times.
reply
You said something false and important and I took the opportunity to educate anyone reading about why this aspect of their computing experience is a mess. All of that is germane to how we ended up in a situation where someone is calling rustc with a Dockerfile and this is considered normal.
reply
…surprisingly sinister agendas.

Dynamic linking moves control from users to vendors and governments at ruinous cost in performance, props up bloated industries...

This is ridiculous. Not everything is a conspiracy!

reply
I didn't say anything was a conspiracy, let alone everything. I said inferior software is promoted by vendors on Linux as well as on MacOS and Windows with unpleasant consequences for users in a way that serves those vendors and the even more powerful institutions to which they are beholden. Sinister intentions are everywhere in this business (go read the opinions of the people who run YC), that's not even remotely controversial.

If fact, if there was anything remotely controversial about a bunch of extremely specific, extremely falsifiable claims I made, one imagines your rebuttal would have mentioned at least one.

I said inflmatory things (Docker is both arsonist and fireman at ruinous cost), but they're fucking true. That Alpine in the Docker jank? Links musl!

reply
That's an even more reasonable fear than trusting trust, and people seem to take that seriously.
reply
Not 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.
reply
For 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() {}
reply
> Once you add static linking to the toolchain (in all of its forms) things get really fucking slow.

Could you expand on that, please? Every time you run dynmically linked program, it is linked at runtime. (unless it explicitly avoids linking unneccessary stuff by dlopening things lazily; which pretty much never happens). If it is fine to link on every program launch, linking at build time should not be a problem at all.

If you want to have link time optimization, that's another story. But you absolutely don't have to do that if you care about build speed.

reply
The 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.

reply
> This has tradeoffs: increased ABI stability at the cost of longer compile times.

Nah. Slow type checking in Swift is primarily caused by the fact that functions and operators can be overloaded on type.

Separately-compiled generics don't introduce any algorithmic complexity and are actually good for compile time, because you don't have to re-type check every template expansion more than once.

reply
A lot can be done by the programmer to mitigate slow builds in Swift. Breaking up long expressions into smaller ones and using explicit types where type inference is expensive for example.

I’d like to see tooling for this to pinpoint bottlenecks - it’s not always obvious what’s making builds slow.

reply
>I’d like to see tooling for this to pinpoint bottlenecks - it’s not always obvious what’s making builds slow.

I second this enthusiastically.

reply
I'll third it. I've started to see more and more cargo culting of "fixes" that I'm extremely suspicious do nothing aside from making the code bulkier.
reply
> Breaking up long expressions into smaller ones

If it improves compile time, that sounds like a bug in the compiler or the design of the language itself.

reply
>This is unlike C++ templates which are effectively copy-pasting the resolved type with the generic for every occurrence of type resolution.

Even this can lead to unworkable compile times, to the point that code is rewritten.

reply
>Codegen is also a problem. Rust tends to generate a lot more code than C or C++

Wouldn't you say a lot of that comes from the macros and (by way of monomorphisation) the type system?

reply
Modern C++ in particular does a lot of similar, albeit not identical, codegen due to its extensive metaprogramming facilities. (C is, of course, dead simple.) I've never looked into it too much but anecdotally Rust does seem to generate significantly more code than C++ in cases where I would intuitively expect the codegen to be similar. For whatever reason, the "in theory" doesn't translate to "in practice" reliably.

I suspect this leaks into both compile-time and run-time costs.

reply
Go is static by default and still fast as hell
reply
Delphi is static by default and incredibly fast too.
reply
That 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.
reply
> 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.

reply
It'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.

reply
This 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/...
reply
I 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.

reply
Did 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`.
reply
Side 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

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.

reply
A ton of that is actually still doing codegen (for the proc macros for example).
reply
deleted
reply
Yes 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.

reply
Is 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.
reply
Digging 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.

reply
Not 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.

reply
Thats 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.
reply
> Go has sub-second build times even on massive code-bases.

Unless you use sqlite, in which case your build takes a million years.

reply
Yeah, 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.

reply
Dlang 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.
reply