原文连接: https://github.com/sxs2473/go...
本文使用 Creative Commons Attribution-ShareAlike 4.0 International 协议进行受权许可。
本节介绍Go编译器执行的三个重要优化。git
Go 编译器在2007年左右开始做为 Plan9 编译器工具链的一个分支。当时的编译器与 Aho 和 Ullman 的 Dragon Book 很是类似。程序员
2015年,当时的 Go 1.5 编译器 从 C 机械地翻译成 Go。github
一年后,Go 1.7 引入了一个基于 SSA 技术的 新编译器后端 ,取代了以前的 Plan 9风格的代码。这个新的后端为泛型和体系结构特定的优化提供了许多可能。golang
咱们要讨论的第一个优化是逃逸分析。后端
为了说明逃逸分析,首先让咱们来回忆一下在 Go spec 中没有提到堆和栈,它只提到 Go 语言是有垃圾回收的,但也没有说明如何是如何实现的。安全
一个遵循 Go spec 的 Go 实现能够将每一个分配操做都在堆上执行。这会给垃圾回收器带来很大压力,但这样作是绝对错误的 -- 多年来,gccgo对逃逸分析的支持很是有限,因此才致使这样作被认为是有效的。函数
然而,goroutine 的栈是做为存储局部变量的廉价场所而存在;没有必要在栈上执行垃圾回收。所以,在栈上分配内存也是更加安全和有效的。工具
在一些语言中,如C
和C++
,在栈仍是堆上分配内存由程序员手动决定——堆分配使用malloc
和free
,而栈分配经过alloca
。错误地使用这种机制会是致使内存错误的常见缘由。优化
在 Go 中,若是一个值超过了函数调用的生命周期,编译器会自动将之移动到堆中。咱们管这种现象叫:该值逃逸到了堆。ui
type Foo struct { a, b, c, d int } func NewFoo() *Foo { return &Foo{a: 3, b: 1, c: 4, d: 7} }
在这个例子中,NewFoo
函数中分配的 Foo
将被移动到堆中,所以在 NewFoo
返回后 Foo
仍然有效。
这是从早期的 Go 就开始有的。与其说它是一种优化,不如说它是一种自动正确性特性。没法在 Go 中返回栈上分配的变量的地址。
同时编译器也能够作相反的事情;它能够找到堆上要分配的东西,并将它们移动到栈上。
让咱们来看下面的例子:
// Sum 函数返回 0-100 的整数之和 func Sum() int { const count = 100 numbers := make([]int, count) for i := range numbers { numbers[i] = i + 1 } var sum int for _, i := range numbers { sum += i } return sum }
Sum
将 0-100 的 ints
型数字相加并返回结果。
由于 numbers
切片仅在 Sum
函数内部使用,编译器将在栈上存储这100个整数而不是堆。也没有必要对 numbers
进行垃圾回收,由于它会在 Sum
返回时自动释放。
证实它!
要打印编译器关于逃逸分析的决策,请使用-m
标志。
% go build -gcflags=-m examples/esc/sum.go # command-line-arguments examples/esc/sum.go:8:17: Sum make([]int, count) does not escape examples/esc/sum.go:22:13: answer escapes to heap examples/esc/sum.go:22:13: main ... argument does not escape
第8行显示编译器已正确推断 make([]int, 100)
的结果不会逃逸到堆。
第22行显示answer
逃逸到堆的缘由是fmt.Println
是一个可变函数。 可变参数函数的参数被装入一个切片,在本例中为[]interface{}
,因此会将answer
赋值为接口值,由于它是经过调用fmt.Println
引用的。 从 Go 1.6(多是)开始,垃圾收集器须要经过接口传递的全部值都是指针,编译器看到的是这样的:
var answer = Sum() fmt.Println([]interface{&answer}...)
咱们可使用标识 -gcflags="-m -m"
来肯定这一点。会返回:
examples/esc/sum.go:22:13: answer escapes to heap examples/esc/sum.go:22:13: from ... argument (arg to ...) at examples/esc/sum.go:22:13 examples/esc/sum.go:22:13: from *(... argument) (indirection) at examples/esc/sum.go:22:13 examples/esc/sum.go:22:13: from ... argument (passed to call[argument content escapes]) at examples/esc/sum.go:22:13 examples/esc/sum.go:22:13: main ... argument does not escape
总之,不要担忧第22行,这对咱们的讨论并不重要。
这个例子是咱们模拟的。 它不是真正的代码,只是一个例子。
package main import "fmt" type Point struct{ X, Y int } const Width = 640 const Height = 480 func Center(p *Point) { p.X = Width / 2 p.Y = Height / 2 } func NewPoint() { p := new(Point) Center(p) fmt.Println(p.X, p.Y) }
NewPoint
建立了一个 *Point
指针值 p
。 咱们将p
传递给Center
函数,该函数将点移动到屏幕中心的位置。最后咱们打印出 p.X
和 p.Y
的值。
% go build -gcflags=-m examples/esc/center.go # command-line-arguments examples/esc/center.go:10:6: can inline Center examples/esc/center.go:17:8: inlining call to Center examples/esc/center.go:10:13: Center p does not escape examples/esc/center.go:18:15: p.X escapes to heap examples/esc/center.go:18:20: p.Y escapes to heap examples/esc/center.go:16:10: NewPoint new(Point) does not escape examples/esc/center.go:18:13: NewPoint ... argument does not escape # command-line-arguments
尽管p
是使用new
分配的,但它不会存储在堆上,由于Center
被内联了,因此没有p
的引用会逃逸到Center
函数。
在 Go 中,函数调用有固定的开销;栈和抢占检查。
硬件分支预测器改善了其中的一些功能,但就功能大小和时钟周期而言,这仍然是一个成本。
内联是避免这些成本的经典优化方法。
内联只对叶子函数有效,叶子函数是不调用其余函数的。这样作的理由是:
还有一个缘由就是严重的内联会使得堆栈信息更加难以跟踪。
func Max(a, b int) int { if a > b { return a } return b } func F() { const a, b = 100, 20 if Max(a, b) == b { panic(b) } }
咱们再次使用 -gcflags = -m
标识来查看编译器优化决策。
% go build -gcflags=-m examples/max/max.go # command-line-arguments examples/max/max.go:3:6: can inline Max examples/max/max.go:12:8: inlining call to Max
编译器打印了两行信息:
Max
的声明告诉咱们它能够内联Max
的主体已经内联到第12行调用者中。编译 max.go
而后咱们看看优化版本的 F()
变成什么样了。
% go build -gcflags=-S examples/max/max.go 2>&1 | grep -A5 '"".F STEXT' "".F STEXT nosplit size=1 args=0x0 locals=0x0 0x0000 00000 (/Users/dfc/devel/gophercon2018-performance-tuning-workshop/4-compiler-optimisations/examples/max/max.go:10) TEXT "".F(SB), NOSPLIT, $0-0 0x0000 00000 (/Users/dfc/devel/gophercon2018-performance-tuning-workshop/4-compiler-optimisations/examples/max/max.go:10) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x0000 00000 (/Users/dfc/devel/gophercon2018-performance-tuning-workshop/4-compiler-optimisations/examples/max/max.go:10) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x0000 00000 (<unknown line number>) RET 0x0000 c3
一旦Max
被内联到这里,这就是F的主体 - 这个函数什么都没干。我知道屏幕上有不少没用的文字,可是相信个人话,惟一发生的就是RET
。实际上F
变成了:
func F() { return }
注意 : 利用 -S
的输出并非进入二进制文件的最终机器码。连接器在最后的连接阶段进行一些处理。像FUNCDATA
和PCDATA
这样的行是垃圾收集器的元数据,它们在连接时移动到其余位置。 若是你正在读取-S
的输出,请忽略FUNCDATA
和PCDATA
行;它们不是最终二进制的一部分。
使用-gcflags=-l
标识调整内联级别。有些使人困惑的是,传递一个-l
将禁用内联,两个或两个以上将在更激进的设置中启用内联。
-gcflags=-l
,禁用内联。-gcflags='-l -l'
内联级别2,更积极,可能更快,可能会制做更大的二进制文件。-gcflags='-l -l -l'
内联级别3,再次更加激进,二进制文件确定更大,也许更快,但也许会有 bug。-gcflags=-l=4
(4个 -l
) 在 Go 1.11 中将支持实验性的 中间栈内联优化。为何a
和b
是常数很重要?
为了理解发生了什么,让咱们看一下编译器在把Max
内联到F
中的时候看到了什么。咱们不能轻易地从编译器中得到这个,可是直接手动完成它。
Before:
func Max(a, b int) int { if a > b { return a } return b } func F() { const a, b = 100, 20 if Max(a, b) == b { panic(b) } }
After:
func F() { const a, b = 100, 20 var result int if a > b { result = a } else { result = b } if result == b { panic(b) } }
由于a
和b
是常量,因此编译器能够在编译时证实分支永远不会是假的;100
老是大于20
。所以它能够进一步优化 F
为
func F() { const a, b = 100, 20 var result int if true { result = a } else { result = b } if result == b { panic(b) } }
既然分支的结果已经知道了,那么结果的内容也就知道了。这叫作分支消除。
func F() { const a, b = 100, 20 const result = a if result == b { panic(b) } }
如今分支被消除了,咱们知道结果老是等于a
,而且由于a
是常数,咱们知道结果是常数。 编译器将此证实应用于第二个分支
func F() { const a, b = 100, 20 const result = a if false { panic(b) } }
而且再次使用分支消除,F
的最终形式减小成这样。
func F() { const a, b = 100, 20 const result = a }
最后就变成
func F() { }
分支消除是一种被称为死码消除的优化。实际上,使用静态证实来代表一段代码永远不可达,一般称为死代码,所以它不须要在最终的二进制文件中编译、优化或发出。
咱们发现死码消除与内联一块儿工做,以减小循环和分支产生的代码数量,这些循环和分支被证实是不可到达的。
你能够利用这一点来实现昂贵的调试,并将其隐藏起来
const debug = false
结合构建标记,这可能很是有用。
编译器标识提供以下:
go build -gcflags=$FLAGS
研究如下编译器功能的操做:
-S
打印正在编译的包的汇编代码-l
控制内联行为; -l
禁止内联, -l -l
增长-l
(更多-l
会增长编译器对代码内联的强度)。试验编译时间,程序大小和运行时间的差别。-m
控制优化决策的打印,如内联,逃逸分析。-m
打印关于编译器的想法的更多细节。-l -N
禁用全部优化。注意 : If you find that subsequent runs of go build ...
produce no output, delete the ./max
binary in your working directory.