Inspecting GO's Compiled Code

I wanted to learn a few things about concurrency before I graduate so I'm on a mission to look at Go and Rust. First stop is Go. Go has a lot of cool features, but the most interesting are related to goroutines and channels. I also want to see how they make goroutines and channel operations fast. And of course their compiler.

Their compiler first parses GO source and compiles x86 machine code as the VM traverses the AST. It's a surprisingly simple compiler that does not seem to perform a whole lot of optimizations. It very much has the flavor of a subroutine threaded interpreter. Lots of calls to the C runtime to do most of the heavy lifting. Consider the following GO source code:

   1:  func sum(a []int, c chan int) {
   2:      sum := 0
   3:       for _, v := range a {
   4:               sum += v
   5:       }
   6:       c <- sum  // send sum to c
   7:  }

Given an array of integers, iterate over the array and sum up all the elements in the array. Instead of returning the value of sum, we send it over a channel. Now let's take a look at the generated machine code:

   1:  JMP     3 // jump into loop
   2:  JMP     18 // Jump out of loop
   3:   
   4:  // Compare if we're done with loop
   5:  INCL    AX
   6:  CMPL    AX,DI
   7:  JGE     2
   8:   
   9:  // Loop body
  10:  MOVL    (CX),BP
  11:  ADDQ    $4,CX
  12:  ADDL    BP,SI
  13:   
  14:  // Jump back to loop header
  15:  JMP     5
  16:   
  17:  // Past loop
  18:  MOVQ    $type.chan int+0(SB),(SP)
  19:  ...
  20:  CALL    ,runtime.chansend1+0(SB)

It's a very weird control flow graph. Most loops look like this:

But go adds another block:

Go's loop exit condition actually jumps to another jump instruction which finally jumps to the point after the loop. What's going on? Why would there be a second jump unless it's just bad optimization or a bug. Maybe it's because the for loop is built to iterate over both the key and value of the array, not just the value? There's another block to test something on the keys? Let's try changing the GO source code to:

   1:  for i, v := range a {
   2:      sum += v
   3:      sum += i
   4:  }

And the result:

   1:  JMP     3
   2:  JMP     18

We still have the double jump.