Golang 编译优化

2022/02/17 技术 共 5731 字,约 17 分钟

Go 编译优化

引子

想要了解编译优化的原理首先要先明白Go到底是怎么编译的。

Tips :

  1. Go的编译器是由Go语言实现,存放在SDK的src/cmd/compile目录下
  2. 通过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的编译器做了什么

  1. 编译器读取源文件,进行词法分析、语法分析、编译优化后生成汇编码(GoAsm)
  2. 汇编器读取生成的汇编文件再次进行编译,输出可重定位目标文件
  3. 链接器将可重定位目标进行链接最终生成可执行目标文件

我们可以看到Go的编译优化过程是在整个编译过程的第1步实现的,接下来我们来看一下Go的编译优化

  1. 词法分析会将源文件的关键字转为一个个Token。
  2. 语法分析过程进一步将Token转化为抽象语法树(AST),在这一过程会进行类型检查。
  3. 优化过程主要基于AST完成了逃逸分析和内联优化,最终输出的依然是AST。
  4. 最后SSA编译器将AST转化成静态单赋值语句,然后完成死码消除,并最终转换为汇编码。

Tips :

  1. SSA(Static Single Assignment)静态单赋值语句顾名思义,每个变量只能被定义一次,可被多次使用。
  2. SSA编译器完成了树结构转线形结构,为后期生成汇编码打基础。
  3. SSA编译过程示例(通过GOSSAFUNC指定生成)传送门

Go的编译优化手段

1. 内联

函数的调用是有开销的,当编译器认为某个函数/方法可以内联到调用者内部,然后将该方法的逻辑直接在调用的位置展开。

但是会有以下几种情况不会进行内联(不全):

  1. 编译使用了禁用参数-l或者禁用内联的注释go:noinline
  2. 待内联的函数内部有特殊关键字(如:for、select等)
  3. 内联后cost大于80(这里的cost指抽象语法树的节点数)

能否内联的策略在cmd/compile/internal/inline/inl.go文件中实现,有兴趣的可以自行查看。

2. 逃逸分析

当一个局部变量被外部使用的时候,会触发内存逃逸。即原本在栈上存储的变量会被移动到堆上。内存逃逸是不会被编译器主动优化掉的,编译器只提供了内存逃逸分析。

内存逃逸导致堆区的内存大量使用,给GC(垃圾回收)带来很大的压力,极有可能造成内存泄漏。所以我们利用逃逸分析去规避这种风险。

内存逃逸发生的几种情况:

  1. 入参是interface{},编译阶段无法确定类型只能分配到堆上。
  2. 局部变量被外部使用
  3. 闭包产生逃逸
  4. 变量大小无法确定导致逃逸
  5. 栈区内存不足导致逃逸

内存逃逸分析相关的代码在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禁用优化

参考文档

文档信息

Search

    Table of Contents