2023-12-18
In Go, constant variables are not used for optimization
Recently I wrote about partially emulating #ifdef with build tags and consts, exploiting Go's support for dead code elimination, and I said that this technique didn't work with variables. That's actually a somewhat interesting result. To see how it is, let's start with a simple Go program, where the following code is the entire program:
package main import "fmt" var doThing bool func main() { fmt.Println("We may or may not do the thing.") if doThing { fmt.Println("We did the thing.") } }
Here, 'doThing
' is a boolean variable that is left at a zero value
(false), and isn't exported on top of being in the 'main' package.
There's nothing in the Go specification that allows the false value
of 'doThing
' to ever change. Despite this, if you inspect the
resulting code the if
and its call to 'fmt.Println()
' is still
present. If you go in with a debugger
and manually set doThing
to true, this code will run.
If you feed a modern C compiler a similar program, with 'doThing
'
declared as a static int, what you get back is code that has optimized
out the code guarded by 'doThing
'. The C compiler knows that the
rules of the C abstract machine don't permit
'doThing
' to change, so it has optimized accordingly. Functionally
your 'static int doThing;
' is now a constant, so the C compiler
has then proceeded to do dead code elimination. The C compiler
doesn't care that you could, for example, go in with a debugger and
want to change the value of 'doThing
', because the existence of
debuggers is not included in the C abstract machine.
(This focus of C optimization on the C abstract machine and nothing beyond it is somewhat controversial, to put it one way.)
Go could have chosen to optimize this case in the same way as C
compilers do, but for whatever reasons the Go developers didn't
choose to do so. One possible motivation to not do this is the
case of debuggers, where you can manually switch 'doThing
' on at
runtime. Another possible motivation is simply to speed up compiling
Go code and to keep the compiler simpler. A C compiler needs a
certain amount of infrastructure so that it knows that the static int
'doThing
' never has its value changed, and then to propagate that
knowledge through code generation; Go doesn't.
Well actually that's a bit of a white lie. The normal Go toolchain
doesn't do all of this with these constant variables, but there's
also gccgo, a Go implementation
that's a frontend for GCC (along side C, C++, and some others).
Since gccgo is built on top of GCC, it can inherit all of GCC's C
focused optimizations, such as recognizing constant variables, and
if you invoke gccgo with the optimization level high enough, it
will optimize the 'doThing
' guarded expression out just like C (this omits the first call to
fmt.Println to make the generated code slightly clearer).
(There have been some efforts to build a Go toolchain based on LLVM, and I'd expect such a toolchain to also optimize this Go code the way gccgo does.)