Hasn't this been a very long time coming? Iirc there has been much dispute over this in the community. Can anyone weigh in on what the other options were?
Hasn't this been a very long time coming? Iirc there has been much dispute over this in the community. Can anyone weigh in on what the other options were?
About how well that decision will turn out in practice, we'll start to know soon.
To pick one of my examples, D templates and compile time metaprogramming are even more powerfull than Rust and C++ ones, yet it compiles just as fast.
So it had them roughly as long as Go existed
However, for code generation Go obviously uses registers since way before the 1.0 days.
In general discussion I see around is that all Rust's perf optimizations are to Rust's credit (nothing about LLVM optimization) while all woes around Rust's compile time lay at step of LLVM and Rust is not be blamed.
As for the comment about MSVC, it's mostly off topic, doesn't relate to the question that was asked (which had nothing to do with which compiler provides the best experience), and even if you accept that MSVC provides the best module experience, it's support for it is still very buggy and not suitable for anything other than experimenting and beta testing.
Modules don't make instantiating templates any faster, it only makes parsing them faster.
Mind you, C/C++’s ridiculous header system is probably still a much bigger issue. Especially because C++ templates usually need to go in header files.
I read about how .Net does Generics at the VM level and was very impressed, a good compromise between C++ "macro expansion" and Java "type erasure", both of which seem extreme.
At the end of the day, though, when generics are present I stop worrying and just use List<int>, List<float>, List<char>, etc. without a second thought. The compiler, VM or runtime can deal with it although I might get punished in some way.
It's not the monomorphization itself that takes a long time, it's the LLVM codegen due to all the extra code being generated from duplicated function implementations. I've heard macros slow for largely the same reason, it's not the macro execution itself that's slow, it's compiling all the generated code.
If that were true, the same issue would apply to hygienic macros, yet it doesn't. It's distinctly a problem with proc macros.
IDK about that. A project I'm working on is template-heavy in a major way, entirely header-only (~15kloc of this: https://github.com/celtera/avendish/blob/main/include/avnd/c... more or less) but with a barely correctly set-up dev environment with clang and PCH (a couple lines in CMake), ninja and mold, individual rebuilds are pretty much instant ; a complete rebuild which on my machine builds 49 libraries and 38 executables takes a whopping 7 seconds, CMake included.
> we were biased too much by experience with C++ without concepts and Java generics.
https://go.googlesource.com/proposal/+/master/design/go2draf...
Most people are really bad at estimating the implementation complexity and tradeoffs inherent in various language features. I'd say that C++ serves as a warning to others... think before you add features to your language, or you'll end up a total mess, like C++. Java and C# gave us some interesting and subtle lessons about what does and does not work about language features like generics. C++'s templates are just a total mess, all around. Java's generics are kind of a funny compile-time feature and have a ton of limitations. C# generics have some downright nasty interactions with other parts of the type system, like operator overloading.
IIRC the designers of C# have some regrets about how generics and other features were implemented. This is from an in-person talk, I don't have anything to cite.
You should take almost nobody at face value when they talk about language features like generics, because there is almost nobody with direct experience both designing and implementing those features. You should be pretty skeptical about what I'm saying too, IMO. But do believe that the design tradeoffs for generics are a complicated enough to justify the wait.
The Golang approach seems to strike a balance between extreme approaches. The C++ approach is "pay for what you use" which, in practice, is actually an extremist language design philosophy. In C++ / Rust, you are supposed to pay for templates only in code size and not in runtime. Everything is fully instantiated. The Haskell approach is "one abstraction fits all, everything is a pointer, use an implementation dictionary, monomorphization is an optimization". Again, something of an extremist approach (similar to Java's, but the comparison with Go / Haskell is better because Go / Haskell both use implementation dictionaries).
A middle approach is actually somewhat novel, believe it or not.
I'm sure that's possible in other languages, but the program I was trying to compile wasn't that complicated... It had just been written by someone who believed every concrete class needed an abstract interface it was implementing.
I remember there was some simple way to crash a Haskell compiler with an out of memory error by defining a series of types, where each successive type required twice as much memory as the previous type to represent.
It's a real problem.
Anecdotally: I maintain a Debian package with a large-ish (but far from huge), template-heavy C++ codebase. I had to give up on compiling on 32 bit machines long ago, simply because not enough addressable memory was available to the compiler. I know other maintainers have to heavily tune the compiler to work around the problem, too.
I was once asked by the Ubuntu folks to disable parallel builds altogether, due to even their 64 bit builders running out of memory altogether.
And this package is just large-ish – it's nowhere near truly large (like a browser or an office suite).
Here for example: https://godbolt.org/z/4nzPY3h57 javac doesn't even bother to resolve that static field at compile time, which wouldn't even have observable side effects. Nope, just blindly emits a static initializer to invoke square(2) at runtime.
Or for a real fun one, enums: https://godbolt.org/z/xjbYvc8dx
Now some of that is necessary to handle all the stuff other classes could do with the enum, but some of it is just unnecessary. Like building the values array, which invokevirtuals all the ordinals of all the enum values. Ordinals the compiler already knew, since it's literally in the same .class file further up (and these are always in the same .class file, they are in lock-step, so there's again no observable difference if the values array was defined with constants instead of invoking ordinals on each field).
And then the trivial switch statement there isn't optimized at all, either, which again wouldn't be observable (compare against what eg. gcc would produce: https://godbolt.org/z/414rGjbE1 )
Further note that these examples are both static initializers, which means the JIT never "fixes" these (and thus instead perpetually contribute to slow startup). So if javac was going to bother with any attempt to optimize at all, you'd think it'd be here.
Bad bytecode is the kludge of byte code you get when you try to write the same thing in kotlin: https://godbolt.org/z/9sGa6csa6
And the bytecode is of comparable quality to javac's.
Operators are static methods so why is that surprising or nasty?
Just take a look at the rules for method overloading in C#, or take a look at one of the various C# compilers to see how methods are resolved.
That's a feature not a bug of golang. Major language changes like this by design are supposed to take a lot of time and thought to land.