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


延伸阅读

相关文章
相关标签/搜索