乐趣区

关于golang:图文讲解Go-中的循环是如何转为汇编的

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

第一工夫关注 Go 技术干货!

本文基于 Go 1.13 版本

循环在编程中是一个重要的概念,且易于上手。然而,循环必须被翻译成计算机能了解的底层指令。它的编译形式也会在肯定水平上影响到规范库中的其余组件。让咱们开始剖析循环吧。

循环的汇编代码

应用循坏迭代 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 开始,这条指令的作用能够通过第二行来判断:

接下来的指令 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://studygolang.com/

喜爱明哥文章的同学

欢送长按下图订阅!

⬇⬇⬇

退出移动版