图文讲解:Go 中的循环是如何转为汇编的?

点击上方“Go编程时光”,选择“加为星标”git

第一时间关注Go技术干货!github

图片

本文基于 Go 1.13 版本 golang

循环在编程中是一个重要的概念,且易于上手。可是,循环必须被翻译成计算机能理解的底层指令。它的编译方式也会在必定程度上影响到标准库中的其余组件。让咱们开始分析循环吧。编程

循环的汇编代码

使用循坏迭代 arrayslicechannel,如下是一个使用循环对 slice 计算总和的例子。安全

`func main() {`
 `l := []int{9, 45, 23, 67, 78}`
 `t := 0`
 `for _, v := range l {`
 `t += v`
 `}`
 `println(t)`
`}`

使用 go tool compile -S main.go 生成的汇编代码,如下为相关输出:架构

`0x0041 00065 (main.go:4)   XORL   AX, AX`
`0x0043 00067 (main.go:4)   XORL   CX, CX`
`0x0045 00069 (main.go:7)   JMP    82`
`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`
`0x0052 00082 (main.go:7)   CMPQ   AX, $5`
`0x0056 00086 (main.go:7)   JLT    71`
`0x0058 00088 (main.go:11)  MOVQ   CX, "".t+8(SP)`

我把这些指令分为了两个部分,初始化部分和循环主体。前两条指令,将两个寄存器初始化为零值。框架

`0x0041 00065 (main.go:4)   XORL   AX, AX`
`0x0043 00067 (main.go:4)   XORL   CX, CX`

寄存器 AX 包含着当前循环所处位置,而 CX 包含着变量 t 的值,下面为带有指令和通用寄存器的直观表示:函数

图片

循环从表示「跳转到指令 82 」的 JMP 82 开始,这条指令的做用能够经过第二行来判断: oop

图片

接下来的指令 CMPQ AX,$5 表示「比较寄存器 AX5」,事实上,这个操做是把 AX 中的值减去 5 ,而后储存在另外一个寄存器中,这个值能够被用在下一条指令 JLT 71 中,它的含义是 「若是值小于 0 则跳转到指令 71 」,如下是更新后的直观表示:优化

图片

若是不知足条件,则程序将会跳转到循环体以后的下一条指令执行。

因此,咱们如今有了对循环的基本框架,如下是转换后的 Go 循环:

`goto end`
`start:`
 `?`
`end:`
 `if i < 5 {`
 `goto start`
 `}`
`println(t)`

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

`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[1] 中到更详细的讨论。


via: https://medium.com/a-journey-...

做者:Vincent Blanchon[2]译者:Jun10ng[3]校对:polaris1119[4]

本文由 GCTT[5] 原创编译,Go 中文网[6] 荣誉推出

参考资料

[1]

Proposal: https://github.com/golang/pro...

[2]

Vincent Blanchon: https://medium.com/@blanchon....

[3]

Jun10ng: https://github.com/Jun10ng

[4]

polaris1119: https://github.com/polaris111...

[5]

GCTT: https://github.com/studygolan...

[6]

Go 中文网: https://studygolang.com/

图片

喜欢明哥文章的同窗

欢迎长按下图订阅!

⬇⬇⬇

图片

相关文章
相关标签/搜索