Golang的i++牽出來的那些事


i++掃盲

猜,下面程序會輸出什麼?html

func main() {
  i := 7758
  j := i++
  fmt.Println(i,j)
}

在沒有遇到過以前,筆者也以爲這是大學生的期末考試題,認爲該程序會輸出7759 7758,由於i++常規操做是先用後加,因此j就是7758,i就是7759。golang

可是正確答案是會報錯,確切的說這段程序在編譯期間就會出錯,將這段代碼放到IDE就會發現爆紅。瀏覽器

這是由於Go中的i++不一樣於C中的i++,Go中的i++是語句,而C中的i++則是表達式。談下我所理解的表達式和語句的區別:函數

表達式是一段能夠被求值的代碼,也就是能夠有接收者;而語句是能夠被執行的代碼,不必定會有接收者。從上面的例子來看,Go中的i++是語句,它不能有接收者,至關於一條能夠被編譯器識別的命令,相似於break,goto這種語句,因此在程序在編譯期間就會報錯。字體

既然原理不一樣,筆者就想經過彙編來對比下C的i++與Go的i++二者有什麼不一樣點。不要聽到彙編就勸退哦,筆者列舉的都是很簡單的語句。優化


對比彙編

接下來簡化下程序,只保留一個聲明和一個自增。ui

//C語言示例
#include <stdlib.h>
int main(){
    int i = 7758;
    i++;
}
//Go語言示例
package main
func main() {
  i := 7758
  i++
}

先把C語言反彙編看下,看下主要部分,能夠看到自增的過程,以下:spa

$ gcc -o plusplustestc -g plusplus.c
$ objdump -S plusplus
......
int i = 7758;
movl $0x1e4e,-0x4(%rbp) #將7758賦值到rbp寄存器
i++;
addl $0x1,-0x4(%rbp)    #將rbp寄存器加1
......

再把Go反彙編看下,發現了奇怪的現象,爲了產生對比效果,我也使用objdump生成彙編語句,發現這裏直接用自增後的7759覆蓋了先前的7758,而這之間並無計算過程。設計

$ go build --gcflags="-l -N" -o plusplustestgo plusplus.go
$ objdump -S plusplustestgo
......
i := 7758
movq $0x1e4e,(%rsp) #將7758賦值到rsp寄存器
i++
movq $0x1e4f,(%rsp) #將7759賦值到rsp寄存器
......

這是由於Go的編譯器作了優化,咱們看到的Plan9彙編這些,都是在編譯最後階段生成的,在這中間編譯器作了大量的優化,省去了許多無用代碼(dead code),好比上述代碼就是Go編譯器SSA(Static Single Assignment靜態單賦值)作的優化,Go語言編譯器在將.go文件編譯爲機器碼過程當中會生成幾十個版本的中間代碼,中間會伴隨着代碼優化,刪除不會被用到的片斷,而上述程序的7758自增爲7759的過程就被編譯器「優化」了,只保留將7759覆蓋到寄存器的過程。code


中間代碼

咱們可使用GOSSAFUNC環境變量構建從源代碼到機器碼這中間幾十次中間代碼的迭代過程,該方法最後會生成ssa.html文件,便於用戶查看,方法以下:

這裏仍然用原來的Go文件示例。

package main
func main() {
  i := 7758
  i++
}

接下來進入該文件的同級目錄下,這裏可能要切換至root權限,執行命令

# 命令以下
# GOSSAFUNC=<函數名> go build <.go文件>
# 實際執行
$ GOSSAFUNC=main go build plusplus.go 
# runtime
dumped SSA to /usr/local/go-1.14/src/runtime/ssa.html
# command-line-arguments
dumped SSA to ./ssa.html

此時中間代碼已經生成到了ssa.html文件中,咱們用瀏覽器打開。能夠經過點擊紅框中的字體查看每一步中間碼的生成,也能夠點擊任意一行代碼查看中間代碼轉化關係。

image.png
image.png

上面倆圖中間還有一長串的中間代碼,這裏就不貼了。

在這裏淺色的字體表明被編譯器」優化「的代碼即dead code,這些代碼不會被編進最後的機器碼中。


可能有些細心的同窗會發現,這裏最終編出來的機器碼genssa中也沒有我上述貼的代碼中賦值寄存器的操做啊,並且怎麼,爲何會形成不一致呢?

這就是 -gcflags="-l -N" 的做用了,在上面生成彙編時候加了這個參數防止內聯(-l)以及編譯優化(-N),因此咱們能夠看到對寄存器賦值的語句。一樣的,咱們也能夠在SSA生成時候加上這個參數,這樣,一些中間代碼就不會被優化掉了,就能夠看到對應的中間代碼了,以下。

$ GOSSAFUNC=main go build --gcflags="-l -N" plusplus.go 
# runtime
dumped SSA to /usr/local/go-1.14/src/runtime/ssa.html
# command-line-arguments
dumped SSA to ./ssa.html

此時再次查看生成的ssa.html,就是禁止內聯以及編譯優化的機器碼的生成步驟了,感興趣的同窗能夠本身嘗試下。

image.png
image.png


延伸閱讀

相關文章
相關標籤/搜索