In Go, constant variables are not used for optimization

December 18, 2023

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.)

Written on 18 December 2023.
« Prometheus's group_left() and group_right() operators
Why grub-install can give you an "unknown filesystem" error »

Page tools: View Source, Add Comment.
Search:
Login: Password:
Atom Syndication: Recent Comments.

Last modified: Mon Dec 18 21:09:55 2023
This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.