圖文講解: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/

圖片

喜歡明哥文章的同窗

歡迎長按下圖訂閱!

⬇⬇⬇

圖片

相關文章
相關標籤/搜索