Notes on exploring the compiler flags in the Go compiler suite

I’ve been doing some work improving the code generation of the 5g compiler, which is the Go compiler for arm. These notes also apply to the 6g and 8g compilers for amd64 and 386 respectively.

For this discussion we’ll use a very simple package.

package addr

func addr(s[]int) *int {
        return &s[2]
}

To see the assembly produced by compiling this package we use the -S flag. -S can be passed directly to the compiler with go tool 5g -S addr.go, but it is simpler (and more portable) to use the -gcflags flag on the go tool itself.

% go build -gcflags=-S addr.go
# command-line-arguments

--- prog list "addr" ---
0000 (/home/dfc/src/addr.go:3) TEXT addr+0(SB),$0-16
0001 (/home/dfc/src/addr.go:4) MOVW $s+0(FP),R0
0002 (/home/dfc/src/addr.go:4) MOVW 4(R0),R1
0003 (/home/dfc/src/addr.go:4) CMP $2,R1,
0004 (/home/dfc/src/addr.go:4) BHI ,6(APC)
0005 (/home/dfc/src/addr.go:4) BL ,runtime.panicindex+0(SB)
0006 (/home/dfc/src/addr.go:4) MOVW 0(R0),R0
0007 (/home/dfc/src/addr.go:4) ADD $8,R0
0008 (/home/dfc/src/addr.go:4) MOVW R0,.noname+12(FP)
0009 (/home/dfc/src/addr.go:4) RET ,

This is quite a lot of code for a one line function. One of the reasons for this is s is a slice, whose length is not known at compile time, so the compiler must insert a bounds check. We can tell the compiler to not emit bounds checks with the -B flag.

% go build -gcflags=-SB addr.go
# command-line-arguments

--- prog list "addr" ---
0000 (/home/dfc/src/addr.go:3) TEXT     addr+0(SB),$0-16
0001 (/home/dfc/src/addr.go:4) MOVW     $s+0(FP),R0
0002 (/home/dfc/src/addr.go:4) MOVW     0(R0),R0
0003 (/home/dfc/src/addr.go:4) ADD      $8,R0
0004 (/home/dfc/src/addr.go:4) MOVW     R0,.noname+12(FP)
0005 (/home/dfc/src/addr.go:4) RET      ,

It is important to note that -B is an unsupported flag. The goal of Go is a safe language, one where array subscripts are bounds checked when they are not provably safe. Go already elides bounds checks when you use range loops, and future compilers will improve this. It is also important to note that none of the builders test -B so it might even generate incorrect code. In summary, when the compiler improves, -B will go away, so don’t get too attached.

One other interesting flag is -N, which will disable the optimisation pass in the compiler

% go build -gcflags=-SN addr.go
# command-line-arguments

--- prog list "addr" ---
0000 (/home/dfc/src/addr.go:3) TEXT     addr+0(SB),$0-16
0001 (/home/dfc/src/addr.go:4) MOVW     $s+0(FP),R0
0002 (/home/dfc/src/addr.go:4) MOVW     R0,R0
0003 (/home/dfc/src/addr.go:4) MOVW     4(R0),R1
0004 (/home/dfc/src/addr.go:4) CMP      $2,R1,
0005 (/home/dfc/src/addr.go:4) BHI      ,8(APC)
0006 (/home/dfc/src/addr.go:4) BL       ,runtime.panicindex+0(SB)
0007 (/home/dfc/src/addr.go:4) UNDEF    ,
0008 (/home/dfc/src/addr.go:4) MOVW     0(R0),R0
0009 (/home/dfc/src/addr.go:4) ADD      $8,R0
0010 (/home/dfc/src/addr.go:4) MOVW     R0,.noname+12(FP)
0011 (/home/dfc/src/addr.go:4) RET      ,
0012 (/home/dfc/src/addr.go:5) BL       ,runtime.throwreturn+0(SB)
0013 (/home/dfc/src/addr.go:5) RET      ,

I think the only thing that is useful about this example is, it’s good thing the optimiser is on by default because there are some strange things going on here, for example line 0002, and the unreachable branch at line 0012.

The last thing to talk about is the output of 5g is not the final code that is executed. Aside from the usual work of a linker, 5l does several transformations on the code which are important to understand.

func addr(s[]int) *int {
   10c00:       e59a1000        ldr     r1, [sl]
   10c04:       e15d0001        cmp     sp, r1
   10c08:       33a01004        movcc   r1, #4
   10c0c:       33a02010        movcc   r2, #16
   10c10:       31a0300e        movcc   r3, lr
   10c14:       3b00668c        blcc    2a64c 
   10c18:       e52de004        push    {lr}            ; (str lr, [sp, #-4]!)
        return &s[2]
   10c1c:       e28d0008        add     r0, sp, #8
   10c20:       e5901004        ldr     r1, [r0, #4]
   10c24:       e3510002        cmp     r1, #2
   10c28:       8a000000        bhi     10c30 
   10c2c:       eb0035d5        bl      1e388 
   10c30:       e5900000        ldr     r0, [r0]
   10c34:       e2800008        add     r0, r0, #8
   10c38:       e58d0014        str     r0, [sp, #20]
   10c3c:       e49df004        pop     {pc}            ; (ldr pc, [sp], #4)

Here we use objdump -dS to dump the addr function as it is compiled into the executable. The first six instructions, starting at 10c00, are the function preamble that deals with segmented stacks which is inserted automatically by the 5l.

Taking it further

There are several other compiler flags which are useful when debugging or optimising your Go code.

  • -g will output the steps a the compiler is a taking at a very low level. The discussion of the output format is outside the scope of this article. Personally I find it easier to add a warn statement which will tell me the source line the compiler was working on at the time.
  • -l will disable inlining (but still retain other compiler optimisations). This is very useful if you are investigating small methods, but can’t find them in objdump.
  • -m is mainly a frontend switch and outputs details about escape analysis and inlining choices.