Go 编译优化
引子
想要了解编译优化的原理首先要先明白Go到底是怎么编译的。
Tips :
- Go的编译器是由Go语言实现,存放在SDK的src/cmd/compile目录下
- 通过go tool compile -help 命令查看编译参数的帮助说明
Go编译过程
通过一个例子了解Go编译过程
file: main.go
package main
import (
"fmt"
)
type A struct {
a int
}
func NewA() *A {
return &A{1}
}
func (a *A) GetA(c *int) *int {
return c
}
func main() {
client := NewA()
var k *int
*k = 1
client.GetA(k)
fmt.Println(client)
}
让我们看看当执行go build main.go时,Go的编译器做了什么

- 编译器读取源文件,进行词法分析、语法分析、编译优化后生成汇编码(GoAsm)
- 汇编器读取生成的汇编文件再次进行编译,输出可重定位目标文件
- 链接器将可重定位目标进行链接最终生成可执行目标文件
我们可以看到Go的编译优化过程是在整个编译过程的第1步实现的,接下来我们来看一下Go的编译优化 
- 词法分析会将源文件的关键字转为一个个Token。
- 语法分析过程进一步将Token转化为抽象语法树(AST),在这一过程会进行类型检查。
- 优化过程主要基于AST完成了逃逸分析和内联优化,最终输出的依然是AST。
- 最后SSA编译器将AST转化成静态单赋值语句,然后完成死码消除,并最终转换为汇编码。
Tips :
- SSA(Static Single Assignment)静态单赋值语句顾名思义,每个变量只能被定义一次,可被多次使用。
- SSA编译器完成了树结构转线形结构,为后期生成汇编码打基础。
- SSA编译过程示例(通过GOSSAFUNC指定生成)传送门
Go的编译优化手段
1. 内联
函数的调用是有开销的,当编译器认为某个函数/方法可以内联到调用者内部,然后将该方法的逻辑直接在调用的位置展开。
但是会有以下几种情况不会进行内联(不全):
- 编译使用了禁用参数
-l或者禁用内联的注释go:noinline - 待内联的函数内部有特殊关键字(如:for、select等)
- 内联后cost大于80(这里的cost指抽象语法树的节点数)
能否内联的策略在cmd/compile/internal/inline/inl.go文件中实现,有兴趣的可以自行查看。
2. 逃逸分析
当一个局部变量被外部使用的时候,会触发内存逃逸。即原本在栈上存储的变量会被移动到堆上。内存逃逸是不会被编译器主动优化掉的,编译器只提供了内存逃逸分析。
内存逃逸导致堆区的内存大量使用,给GC(垃圾回收)带来很大的压力,极有可能造成内存泄漏。所以我们利用逃逸分析去规避这种风险。
内存逃逸发生的几种情况:
- 入参是interface{},编译阶段无法确定类型只能分配到堆上。
- 局部变量被外部使用
- 闭包产生逃逸
- 变量大小无法确定导致逃逸
- 栈区内存不足导致逃逸
内存逃逸分析相关的代码在cmd/compile/internal/escape/escape.go文件中。
3. 死码消除
编译器通过语法分析发现了无用的逻辑(比如if true {})会直接删除。一般死码消除配合内联优化一起生效。在编译阶段优化掉无用的逻辑一方面能够减少目标文件的大小,另一方面也能提高程序的运行效率。
死码消除的相关代码在cmd/compile/internal/deadcode/deadcode.go文件中。
查看Go的编译优化
1. 查看优化决策
通过gcflags='-m'命令查看Go编译的优化决策
# [shell input]
go build -gcflags='-m' main.go
上述命令的输出
# command-line-arguments
./main.go:21:6: can inline NewA # 21行 NewA()方法可以进行内联
./main.go:25:6: can inline (*A).GetA
./main.go:10:16: inlining call to NewA # 10行 NewA进行内联了
./main.go:13:13: inlining call to (*A).GetA
./main.go:14:13: inlining call to fmt.Println
./main.go:10:16: &A{...} escapes to heap # 10行 &A{} 逃逸到了堆上
./main.go:14:13: []interface {}{...} does not escape
./main.go:22:9: &A{...} escapes to heap
./main.go:25:7: a does not escape # 25行 变量a 没有逃逸
./main.go:25:18: leaking param: c to result ~r1 level=0 # 25行 变量c没有任何变动
<autogenerated>:1: .this does not escape
2. 查看编译生成的汇编
通过gcflags='-S'命令查看Go编译生成的汇编
go build -gcflags='-S' main.go 2>&1 |grep -A20 '"".main STEXT'
看一下上述命令的输出
"".main STEXT size=160 args=0x0 locals=0x58 funcid=0x0
0x0000 00000 (/Users/kcjia/Developer/GoPath/src/test/main.go:9) TEXT "".main(SB), ABIInternal, $96-0
0x0000 00000 (/Users/kcjia/Developer/GoPath/src/test/main.go:9) MOVD 16(g), R1
0x0004 00004 (/Users/kcjia/Developer/GoPath/src/test/main.go:9) PCDATA $0, $-2
0x0004 00004 (/Users/kcjia/Developer/GoPath/src/test/main.go:9) MOVD RSP, R2
0x0008 00008 (/Users/kcjia/Developer/GoPath/src/test/main.go:9) CMP R1, R2
0x000c 00012 (/Users/kcjia/Developer/GoPath/src/test/main.go:9) BLS 144
0x0010 00016 (/Users/kcjia/Developer/GoPath/src/test/main.go:9) PCDATA $0, $-1
0x0010 00016 (/Users/kcjia/Developer/GoPath/src/test/main.go:9) MOVD.W R30, -96(RSP)
0x0014 00020 (/Users/kcjia/Developer/GoPath/src/test/main.go:9) MOVD R29, -8(RSP)
0x0018 00024 (/Users/kcjia/Developer/GoPath/src/test/main.go:9) SUB $8, RSP, R29
0x001c 00028 (/Users/kcjia/Developer/GoPath/src/test/main.go:9) FUNCDATA ZR, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x001c 00028 (/Users/kcjia/Developer/GoPath/src/test/main.go:9) FUNCDATA $1, gclocals·f207267fbf96a0178e8758c6e3e0ce28(SB)
0x001c 00028 (/Users/kcjia/Developer/GoPath/src/test/main.go:9) FUNCDATA $2, "".main.stkobj(SB)
0x001c 00028 (<unknown line number>) NOP
0x001c 00028 (/Users/kcjia/Developer/GoPath/src/test/main.go:10) MOVD $type."".A(SB), R0
0x0024 00036 (/Users/kcjia/Developer/GoPath/src/test/main.go:22) MOVD R0, 8(RSP)
0x0028 00040 (/Users/kcjia/Developer/GoPath/src/test/main.go:22) PCDATA $1, ZR
0x0028 00040 (/Users/kcjia/Developer/GoPath/src/test/main.go:22) CALL runtime.newobject(SB)
0x002c 00044 (/Users/kcjia/Developer/GoPath/src/test/main.go:22) MOVD 16(RSP), R0
0x0030 00048 (/Users/kcjia/Developer/GoPath/src/test/main.go:22) MOVD $1, R1
......
可以看到汇编语言16行之后已经将NewA()方法内联进来了,现在我们通过gcflags='-l'来禁用内联
go build -gcflags='-l -S' main.go 2>&1 |grep -A20 '"".main STEXT'
再看一下汇编结果
"".main STEXT size=144 args=0x0 locals=0x58 funcid=0x0
0x0000 00000 (/Users/kcjia/Developer/GoPath/src/test/main.go:9) TEXT "".main(SB), ABIInternal, $96-0
0x0000 00000 (/Users/kcjia/Developer/GoPath/src/test/main.go:9) MOVD 16(g), R1
0x0004 00004 (/Users/kcjia/Developer/GoPath/src/test/main.go:9) PCDATA $0, $-2
0x0004 00004 (/Users/kcjia/Developer/GoPath/src/test/main.go:9) MOVD RSP, R2
0x0008 00008 (/Users/kcjia/Developer/GoPath/src/test/main.go:9) CMP R1, R2
0x000c 00012 (/Users/kcjia/Developer/GoPath/src/test/main.go:9) BLS 124
0x0010 00016 (/Users/kcjia/Developer/GoPath/src/test/main.go:9) PCDATA $0, $-1
0x0010 00016 (/Users/kcjia/Developer/GoPath/src/test/main.go:9) MOVD.W R30, -96(RSP)
0x0014 00020 (/Users/kcjia/Developer/GoPath/src/test/main.go:9) MOVD R29, -8(RSP)
0x0018 00024 (/Users/kcjia/Developer/GoPath/src/test/main.go:9) SUB $8, RSP, R29
0x001c 00028 (/Users/kcjia/Developer/GoPath/src/test/main.go:9) FUNCDATA ZR, gclocals·69c1753bd5f81501d95132d08af04464(SB)
0x001c 00028 (/Users/kcjia/Developer/GoPath/src/test/main.go:9) FUNCDATA $1, gclocals·713abd6cdf5e052e4dcd3eb297c82601(SB)
0x001c 00028 (/Users/kcjia/Developer/GoPath/src/test/main.go:9) FUNCDATA $2, "".main.stkobj(SB)
0x001c 00028 (/Users/kcjia/Developer/GoPath/src/test/main.go:10) PCDATA $1, ZR
0x001c 00028 (/Users/kcjia/Developer/GoPath/src/test/main.go:10) CALL "".NewA(SB)
0x0020 00032 (/Users/kcjia/Developer/GoPath/src/test/main.go:10) MOVD 8(RSP), R0
0x0024 00036 (/Users/kcjia/Developer/GoPath/src/test/main.go:10) MOVD R0, "".client-24(SP)
0x0028 00040 (/Users/kcjia/Developer/GoPath/src/test/main.go:12) MOVD $1, R1
0x002c 00044 (/Users/kcjia/Developer/GoPath/src/test/main.go:12) MOVD ZR, R2
0x0030 00048 (/Users/kcjia/Developer/GoPath/src/test/main.go:12) MOVD R1, (R2)
可以看到16行的内联已经变成了函数调用,因为我们禁用了内联优化。
Q&A
Q:为什么Debug Go的二进制文件时总是有变量看不到?
A:因为编译过程中这些变量被优化掉了
Q:怎么不让编译器优化这些变量呢?
A:编译的时候新增参数gcflags=’-l -N’,-l禁用内联,-N禁用优化
参考文档
文档信息
- 本文作者:KcJia
- 本文链接:https://blog.kcjia.cn/2022/02/17/go-compile/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)