Go 中的循环是如何转为汇编的(方法详解)

2020-05-06 12:24:21王振洲

我们缺少了循环的主体,接下来,我们看看这部分的指令:

0x0047 00071 (main.go:7)   MOVQ   ""..autotmp_5+16(SP)(AX*8), DX
0x004c 00076 (main.go:7)   INCQ   AX
0x004f 00079 (main.go:8)   ADDQ   DX, CX

第一条指令 MOVQ ""..autotmp_5+16(SP)(AX*8), DX 表示 「将内存从源位置移动到目标地址」,它由以下几个部分组成:

""..autotmp_5+16(SP) 表示 slice ,而 SP 表示了栈指针即我们当前的内存空间, autotmp_* 是自动生成变量名。

偏差为 8 是因为在 64 位计算机架构中, int 类型是 8 字节的。 偏差乘以寄存器 AX 的值,表示当前循环中的位置。 寄存器 DX 代表的目标地址内包含着循环的当前值。

之后, INCQ 表示自增,然后会增加循环的当前位置:

 

循环主体的最后一条指令是 ADDQ DX, CX ,表示把 DX 的值加在 CX ,所以我们可以看出, DX 所包含的值是目前循环所代表的的值,而 CX 代表了变量 t 的值。

他会一直循环至计数器到 5 ,之后循环体之后的指令表示为将寄存器 CX 的值赋予 t

0x0058 00088 (main.go:11)   MOVQ   CX, "".t+8(SP)

以下为最终状态的示意图:

我们可以完善 Go 中循环的转换:

func main() {
 l := []int{9, 45, 23, 67, 78}
 t := 0
 i := 0

 var tmp int

 goto end
start:
 tmp = l[i]
 i++
 t += tmp
end:
 if i < 5 {
 goto start
 }

 println(t)
}

这个程序生成的汇编代码与上文所提到的函数生成的汇编代码有着相同的输出。

改进

循环的内部转换方式可能会对其他特性(如 Go 调度器)产生影响。在 Go 1.10 之前,循环像下面的代码一样编译:

func main() {
 l := []int{9, 45, 23, 67, 78}
 t := 0
 i := 0

 var tmp int
 p := uintptr(unsafe.Pointer(&l[0]))

 if i >= 5 {
 goto end
 }
body:
 tmp = *(*int)(unsafe.Pointer(p))
 p += unsafe.Sizeof(l[0])
 i++
 t += tmp
 if i < 5 {
 goto body
 }
end:
 println(t)
}

这种实现方式的问题是,当 i 达到 5 时,指针 p 已经超过了内存分配空间的尾部。这个问题使得循环不容易抢占,因为它的主体是不安全的。循环编译的优化确保它不会创建任何越界的指针。这个改进是为 Go 调度器中的非合作抢占做准备的。你可以在这篇Proposal 中到更详细的讨论。