
The Go Compiler Needs to Be Smarter - hactually
https://lemire.me/blog/2020/06/04/the-go-compiler-needs-to-be-smarter/
======
bitwize
Sure, if your goal is to build a compiler that outputs the fastest possible
code.

If your goal is to apply the "principle of least surprise" to the generated
machine code, then that eliminates or constrains most interesting
optimizations, including procedure inlining, dead code elision, and compile-
time evaluation.

Go is a "principle of least surprise" language, based on a vision of
simplicity for C that hasn't existed among actual C implementations (outside
of maybe Plan 9) for decades now. It's yet more suckless idiocy: sacrifice
everything, including utility and friendliness to end users, to buy
"simplicity" for hackers. No wonder the suckless folks love Go so much.

~~~
dgellow
Suckless is promoting C99 and disregards anything with a GC.

[https://suckless.org/coding_style/](https://suckless.org/coding_style/)

~~~
MaxBarraclough
> All variable declarations at top of block.

And they say they want their C code to suck _less_?

Why voluntarily open the door to read-before-write undefined-behaviour? The
lifetime of a local should be as short as it reasonably can be.

~~~
jagged-chisel
But can't the compiler work that bit out? Source code is for humans. Put all
the vars at the top to tell the humans "here's all the scratch space I'll be
needing in this block" and then let the compiler observe "var x is only used
twice, so let's optimize that lifetime..."

~~~
atilaneves
> Source code is for humans

The reason why making the scope as local as possible is important is for
_humans_ , not compilers.

> Put all the vars at the top to tell the humans "here's all the scratch space
> I'll be needing in this block"

Why would humans care about how much scratch space is needed? That's for the
compiler to know.

~~~
jagged-chisel
We're not talking about scope. The scope is already decided. "Vars at the top
of the block" doesn't mean "take those extra vars out of the while{} scope and
elevate them to the next enclosing scope."

You've taken my "scratch space" too literally. Very few people need to count
bytes for local vars. I'm talking about future maintainers reading and
understanding code. Grouping the current block's variables at the top says
nothing about how the compiler might organize the resulting code and storage.
But it does inform future readers of the code.

~~~
nybble41
The scope of a variable is from where it is declared to the end of the block.
Moving a variable to the top of the enclosing block means that it can be
referenced from more places in the code, which increases its scope.

Warnings about uninitialized variables help, but don't catch everything. For
example, you don't usually get a warning for passing the address of an
uninitialized variable to an external function (since it might be an output
parameter), but that would be undefined behavior if the function expects the
variable to be initialized. Initializing variables at the point where they are
declared ensures that they can't be referenced at all in an uninitialized
state.

Rust has a slightly more nuanced (and IMHO superior) system: non-mutable
("const") variables can be assigned exactly once, possibly but not necessarily
at the point where they are declared, and all variables must be initialized
before use, including passing references to other functions. This permits more
flexibility in how the code is arranged while simultaneously offering stronger
guarantees against undefined or otherwise erroneous behavior.

------
q3k
Does it really, though? Go really isn't meant for number crunching, or any
CPU-bound code, really.

I think it's just fine for a language to not strive to emit the fastest
possible code. I don't mind having an ecosystem where build times are fast in
return for less optimized code. We already have plenty of languages that do
the opposite (Rust, C++, Haskell, ...), and I would personally hate to see
Go's compilation/link times creep up that level.

~~~
Touche
Why am I using a language with pointers if it doesn't care about being fast?

Another problem with not having a smart compiler is the gigantic binaries it
produces. For wasm, an officially supported target, this makes it a non-
starter.

~~~
q3k
> Why am I using a language with pointers if it doesn't care about being fast?

Because, I assume, you want to differentiate between pass-by-value and pass-
by-reference - and pointers are a familiar, simple (if not simplistic) way of
expressing that.

------
bjoli
I find it hard to fathom that many programming languages still don't use
partial evaluation. That a popular language avoids inlining is even beyond
that.

Inlining us hard to do in ways that makes sure you always produce the optimal
code, but simple conservative inlining is probably among the simplest and most
efficient optimisations you can do.

Edit: one of my favourite quotes is about the hardships of the heuristics of
inliners: "I don't know how many of you have tried to write a good inliner. It
is like barely being able to restrain a rabid dog on a leash".

I probably mangled that, but originally it is a quote by Andy Wingo when
talking about the improved optimisations of guile 2.2.

~~~
kjksf
Go does inlining and their inlining heuristic has been tweaked over time.

~~~
bjoli
The example in the article is a typical function where I would expect
inlining. Overly shy seems almost like an understatement.

~~~
Thaxll
You can see the reason actually:

    
    
      go build -gcflags="-m -m" .
      # _/tmp/inline
      ./main.go:7:6: cannot inline sum: unhandled op RANGE
      ./main.go:15:6: can inline fun as: func() uint64 { x := 
      []uint64 literal; return sum(x) }
      ./main.go:3:6: can inline main as: func() { _ = fun() }
      ./main.go:4:9: inlining call to fun func() uint64 { x 
      := []uint64 literal; return sum(x) }
      ./main.go:7:10: sum x does not escape
      ./main.go:4:9: main []uint64 literal does not escape
      ./main.go:16:15: fun []uint64 literal does not escape
    
     package main
    
     func main() {
      _ = fun()
     }
    
     func sum(x []uint64) uint64 {
      var sum = uint64(0)
      for _, v := range x {
       sum += v
      }
      return sum
     }
    
     func fun() uint64 {
      x := []uint64{10, 20, 30}
      return sum(x)
     }
    

Go does not inline range operation.

~~~
uryga
> Go does not inline range operation.

do you know why?

[not _super_ familiar with Go, just curious]

~~~
Thaxll
I'm not sure I think it's just a current limitation:
[https://github.com/golang/go/wiki/CompilerOptimizations#func...](https://github.com/golang/go/wiki/CompilerOptimizations#function-
inlining)

They do improve the compiler every release, so we might see some of those
operations get inlined in the future.

More information there:
[https://github.com/golang/go/blob/master/src/cmd/compile/int...](https://github.com/golang/go/blob/master/src/cmd/compile/internal/gc/inl.go#L10)

------
cogman10
I may have missed it, but this discussion seems to be lacking a key motivation
of go. That is, the go devs have preferred a fast compiler over fast code
output.

I have a hard time imagining them adding a bunch of optimizations around code
generation when those optimizations will almost certainly:

* Slow down compile times

* Increase compiler complexity

* Be worse than what something like LLVM would produce.

Maybe it makes sense to integrate a "release" build which targets LLVM? IDK.
But I do know that so long as compilation speed is a major goal for the
language the you simply won't see optimizations being seriously considered.

~~~
zamadatix
Did you catch the note at the end about how gccgo is slower? If so how does
pivoting to LLVM solve the issues seen there?

~~~
cogman10
You can read about the why here.

[https://stackoverflow.com/questions/25811445/what-are-the-
pr...](https://stackoverflow.com/questions/25811445/what-are-the-primary-
differences-between-gc-and-gccgo)

The short answer is that there are optimizations that the GC is doing which
gccgo is not doing.

The long answer is that gc will do escape analysis which can avoid allocating
on the heap, gccgo doesn't do that.

Why might LLVM be better suited? Primarily because the framework also supports
JIT and has efforts from the likes of Azul to make their JVM faster. Most
optimizations that would benefit Java will benefit go.

[https://llvm.org/devmtg/2017-10/slides/Reames-
FalconKeynote....](https://llvm.org/devmtg/2017-10/slides/Reames-
FalconKeynote.pdf)

------
stunt
Go mostly has been only picking up easy wins so far. I haven't seen any
attempt to go further than that. Which is fair strategy for first few years
since everything comes with a cost. But, people now expect to see more
obviously specially the more it's being compared to more sophisticated
languages and compilers.

~~~
gizzlon
> But, people now expect to see more obviously [..]

I don't. I mean, yay for improvements, but it's already working great for my
usecases.

~~~
dirtydroog
What are your usecases, if you don't mind me asking? I am considering
replacing our HTTP-heavy processes currently written in C++ with Go. However,
after reading this thread I'm not so sure. Compilation time isn't that big of
an issue for us, but having a simpler way of doing networking would be a win.
I can't tell if that ease of use would be trumped by poor performance.

~~~
jerf
It isn't clear to me you're looking at a clear win there. If you do want to
try it out, I recommend the strangler pattern: [https://docs.microsoft.com/en-
us/azure/architecture/patterns...](https://docs.microsoft.com/en-
us/azure/architecture/patterns/strangler) With HTTP it's really easy to
rewrite one URL at a time. You just need some way to proxy things around,
nginx if nothing else, and then you can swap things as you go along, so you
can pick and choose which URLs to test the Go implementation on.

My rule of thumb for Go performance is that's roughly 2-3x slower that C/C++.
While human loss aversion is probably kicking in and making that sound
horrible, from an engineering perspective, it's likely you'll not notice it,
speaking broadly from my position of ignorance. However, if you do have your
code deployed to places that are routinely running the CPU at 50%+ all the
time in your C++ code (as opposed to DB wait or whatever), and you are not
interested in investing in more hardware, I wouldn't even consider switching
to Go.

~~~
dirtydroog
It's mainly binary to JSON conversion, and firing that out over HTTP a few
thousand times a second. That's the only IO. Goroutines look very interesting,
and as I said, ASIO (C++ async networking) is a real pain to work with. But
there are latency requirements here. Something that previously took 1ms cannot
now take 10ms.

------
drej
A frustrating thing about inlining in Go is that there's a fairly arbitrary
cost model and you sometimes end up fighting it (lookup George Tankersley's
talks on YouTube). There have been tons of discussions about whether or not it
should be user configurable, if inlining hints should happen at the call sites
or function definitions etc.

It's quite a nice discussion that helps people understand the toolchain. It
also goes to show that while a self hosted build system is nice, you forgo
decades of gcc/llvm optimisations.
[https://github.com/golang/go/issues/17566](https://github.com/golang/go/issues/17566)

~~~
pjmlp
Those optimizations could be taken advantage of via gccgo.

Also many other languages keep having backend issues, because those
optimizations are too focused on C and C++ code semantics.

~~~
entha_saava
> Also many other languages keep having backend issues, because those
> optimizations are too focused on C and C++ code semantics.

Attribute it to LLVM monoculture.

~~~
jcranmer
Attribute it to benchmarks people care about being written in C/C++ (with
maybe a dash of Fortran). e.g., SPEC CPU.

------
_wldu
___" I do not mind that Go lacks exceptions or generics. These features are
often overrated."_ __

I agree 100%. I 've used them both in C++. I hope Go never gets them.

~~~
alharith
Generics, I would certainly be ecstatic for _if it was done in a way that is
novel and feels Go-like_ , so not Java or C# or C++'s implementations. This
has certainly been the stance of the Go team since the beginning of time. It
has never been "anti-generics" it's always been "we've studied all the
generics implementations out there, and didn't feel like any one of them were
good enough, so rather than shoehorn them in, we are being patient." This
approach needs to be celebrated more.

Exceptions, OTOH, I hope never see the light of day in Go. Curse them.

~~~
jjice
What's wrong with the way that Java, C# a, and C++ handle generics? I see this
complaint fairly regularly, but I was never sure why. Is it how the compiler
handles it, or how the language defines it that is the reason for this
dislike? Genuine curiosity.

~~~
alharith
This is the lazy answer I know, but I wanted to at least answer your genuine
curiosity: There is ample research available online on the subject that
explains it in depth and far better than I ever could. However, I will leave
you with a few quotes that help at least paint the picture.

From the Golang FAQ:

> Generics are convenient but they come at a cost in complexity in the type
> system and run-time. We haven’t yet found a design that gives value
> proportionate to the complexity

Quote from Russ Cox:

> The generic dilemma is this: do you want slow programmers, slow compilers
> and bloated binaries, or slow execution times?

Finally, one of the reasons the C++ or Java approach has never been palatable
to Go core devs is summarized from this Rob Pike quote from his famous "Less
is Exponentially More" blog post:

> If C++ and Java are about type hierarchies and the taxonomy of types, Go is
> about composition

So therefore it really is also a matter of finding a generics approach that
lives up to that spirit as well. Contracts I think are approaching that.

All that said, user defined generics (since technically speaking, Go has many
generic capabilities today already) _are_ coming to Go, they just aren't being
rushed. I think we will be happy with the generics implementation in Go within
the next year.

------
gautamcgoel
The optimization I care about most is vectorization. A lot of ML and data
science basically boils down to numerical linear algebra and optimization;
vectorization is the key optimization for these kinds of problems. I bet if Go
got a better autovectorizer it would compare more favorably against languages
like Rust and C++ for numerical tasks. The trade-off, of course, is longer
compile times, which I suspect will be unacceptable to the core Go team.

~~~
pjmlp
Even Java and .NET do better on auto vectorization.

Intel has recently published an article on using Go, their advice?

Manually use cgo or Go Assembly for the AVX.

~~~
hu3
Apples to orange comparison. Java and .NET have different goals, around 2
decades of optimization and a lot more money thrown at them.

Also, hi again pjmlp! Another Go thread, another nonconstructive Go bashing
coming from you. From your other comments here I see you got pretty combative
this time.

~~~
dang
Please don't ever start or feed flamewars like this on HN again. We ban
accounts that do that, and what you did here was egregious.

[https://news.ycombinator.com/newsguidelines.html](https://news.ycombinator.com/newsguidelines.html)

~~~
hu3
I assume full responsibility. I started it.

Regardless of pjmlp's behavior in Go threads it doesn't justify me lowering
the bar like this.

You wont see this kind of content coming from me in the future.

------
hactually
This[0] PR should fix some of it is my understanding but will have to wait and
see -- not playing with /tip!

[0]
[https://github.com/golang/go/commit/fff7509d472778cae5e652db...](https://github.com/golang/go/commit/fff7509d472778cae5e652dbe2479929c666c24f)

------
gautamcgoel
Another pain point in Go I vaguely recall is that functions written in
assembly are never inlined, so you always pay some penalty for using such
functions (this may have changed in a more recent version of Go).

------
JTenerife
No benchmarks or any other numbers. I wonder how much faster some typical Go
code (some CLIs, Hugo or webservices) will run? Around 1%, 5% or 10%?

The Go compiler doesn't __need __to be smarter for Go 's usecase. If it can be
made smarter, fine. For best possible performance look at C/C++ or Rust.

~~~
jorangreef
"No benchmarks or any other numbers."

And yet plenty of logical argument. The author assumes a level of optimization
experience on the part of the reader, for example that the reader appreciates
the physical cost of branch mispredictions.

------
wwarner
With its well defined AST, I think Go can "easily" accommodate optimizations
as described, but obviously at the expense of compililation time. And I gotta
tell you that keeping compile times down is the more important optimization
for me. However, I happily add automated scans for safety and correctness into
my release candidates, and I would gladly pay for optimizations at that point.
As long as I can test rapidly with the knowledge that I can add an "-O3" flag
just before release would be a perfect compromise for me.

------
ldeangelis
Correct me if I'm wrong but comparing Go against Swift, C, C++, and Rust isn't
really fair since one of Go's goals is compilation speed, in which it seems to
shine against these other languages. From what I understand, you're going to
trade performance against compilation speed, and Go is on the opposite side of
these choices compared to the other languages.

~~~
onei
Realistically, compilation speed is something every major language is going to
try to improve upon to improve adoption.

I haven't looked at the internals of the go compiler, but it seems a bit
simplistic perhaps in the interest of improving speed. For example, it stops
outputting errors after a point when it can clearly go further after having
demonstrated that it's parser can recover. The error messages have lots of
room for improvement, particularly compared to Rust.

~~~
ldeangelis
I think one of the pain points that Go addresses is the compilation speed of
large C++ projects at Google, and it's one of the reasons it was made in the
first place. From what I know C++ and Rust are in the same ballpark in terms
of compilation speed, while Go gives a noticeable improvement.

------
josefx
> In less fanciful languages like C or C++, the programmer needs to check what
> the processor supports themselves.

I think by now most C++ compilers can target multiple combinations of feature
flags and select a supported code path during program startup. The Intel
compiler could do it years ago, but the resulting code was crippled on AMD
CPUs.

------
sitkack
It will be a whole lot smarter on June 8th at 7am pacific,
[https://www.youtube.com/watch?v=Dq0WFigax_c](https://www.youtube.com/watch?v=Dq0WFigax_c)

~~~
pjmlp
I believe it when I see it on
[https://golang.org/doc/devel/release.html](https://golang.org/doc/devel/release.html)

Until then, it is just yet another "we are working on it" like so many that
have come up during the last 10 years.

~~~
sitkack
It has Lambda Man and Griesemer on the case. I think this one will stick
because it was requested by the Go team and there is precedence with
featherweight java and that Wadler designed the generics for Java. [1]

There are prototypes you can play with now.

[https://blog.tempus-ex.com/generics-in-go-how-they-work-
and-...](https://blog.tempus-ex.com/generics-in-go-how-they-work-and-how-to-
play-with-them/)

[1]
[http://homepages.inf.ed.ac.uk/wadler/](http://homepages.inf.ed.ac.uk/wadler/)

------
dirtydroog
This is a very interesting at a time when we're seeing what we can replace C++
with.

(yes, rusticles, I know rust exists)

~~~
panpanna
I think go is mostly used to replace Python and Java & co.

~~~
atilaneves
And yet, it's worse than both.

