前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go 编译器优化

Go 编译器优化

作者头像
gopher云原生
发布2022-11-22 14:59:45
7400
发布2022-11-22 14:59:45
举报
文章被收录于专栏:gopher云原生gopher云原生

《从.go 文本文件到可执行文件》一文中,我们简单描述了 Go 编译器的工作流程。本文将继续深入其中的一些代码优化的工作。

前情回顾

死代码消除

死代码消除( dead code elimination, 缩写 DCE )是用来移除对程序执行结果没有任何影响的代码,以此 减少程序的体积大小 ,并且还可以避免程序在执行过程中进行一些不必要的运算行为,从而 减少执行时间

需要注意的是,除了不会执行到的代码( unreachable code ),一些只会影响到无关程序执行结果的变量( dead variables ),也属于死码( dead code )的范畴。

简单示例:

代码语言:javascript
复制
package main

func main() {
 const a, b = 200, 100
 var max int
 if a > b {
  max = a
 } else {
  max = b
 }
 if max == b {
  panic(b)
 }
}

对于常量 ab ,编译器在编译时可以判断出 a 永远是大于 b 的,即 a > b 永远为 true ,也就是说 else {} 分支属于 unreachable code 将永远不会被执行,所以编译器会进行第一次优化:分支消除

代码语言:javascript
复制
package main

func main() {
 const a, b = 200, 100
 const max = a
 if max == b {
  panic(b)
 }
}

由于 max 变量后续没有再被引用,所以 max 实际也是一个常量。相同道理,max == b 永远为 false ,编译器会进行第二次分支消除优化:

代码语言:javascript
复制
package main

func main() {
 const a, b = 200, 100
 const max = a
}

对于剩下的常量则明显属于 dead variables ,再次优化:

代码语言:javascript
复制
package main

func main() {
}

我们可以查看最初程序的 SSA 生成过程来验证:

代码语言:javascript
复制
$ GOSSAFUNC=main go build main.go

查看生成的 ssa.html

死代码消除过程

最终生成的 SSA

可以看到,main 函数内的所有逻辑确实都被编译器优化掉了。

函数内联

如果程序中存在大量的小函数的调用,函数内联(function call inlining)就会直接用函数体替换掉函数调用来 减少因为函数调用而造成的额外上下文切换开销

简单示例:

代码语言:javascript
复制
package main

func main() {
 n := 1
 for i := 0; i < 10; i++ {
  n = double(n)
 }
 println(n)
}

func double(n int) int {
 return 2 * n
}

对于上面的代码,编译器内联优化后会变成:

代码语言:javascript
复制
package main

func main() {
 n := 1
 for i := 0; i < 10; i++ {
  n = 2 * n
 }
 println(n)
}

Go 编译器会计算函数内联所花费的成本,只有满足相关策略时才会进行内联优化,最简单的当函数内有 godeferselect 等关键字时就不会发生内联,具体的策略可以直接查看源码:

内联优化相关源码

使用 go tool compile -m=2 main.gogo build -gcflags="-m -m" main.go 可以输出内联优化的相关信息( -m 的数量越多输出结果越详细)

代码语言:javascript
复制
$ go tool compile -m=2 main.go
main.go:11:6: can inline double with cost 4 as: func(int) int { return 2 * n }
main.go:3:6: can inline main with cost 28 as: func() { n := 1; for loop; println(n) }
main.go:6:13: inlining call to double

或者也可以输出汇编代码查看是否有进行 double 函数的调用,这里显然是没有的:

代码语言:javascript
复制
$ go tool compile -S main.go | grep CALL.*double

如果我们不想一个函数被内联,可以直接在其函数定义时加一个 //go:noinline 注释:

代码语言:javascript
复制
//go:noinline
func double(n int) int {
 return 2 * n
}

同样可以进行验证:

代码语言:javascript
复制
$ go tool compile -S main.go | grep CALL.*double
        0x0025 00037 (main.go:6)        CALL    "".double(SB)
$ go tool compile -m=2 main.go
main.go:12:6: cannot inline double: marked go:noinline
main.go:3:6: cannot inline main: function too complex: cost 81 exceeds budget 80

可以看到此时还输出了函数无法内联的原因。

如果希望所有函数都不执行内联操作,可以直接为编译器选项加上 -l 参数,即 go build -gcflags="-l" main.go (如果 -l 数量大于等于 2 ,编译器将会采用更激进的内联策略,但也可能会生成更大的二进制文件)。

正常情况,我们直接使用编译器默认选项即可。

逃逸分析

不同于 C 语言的手动内存管理方式(通过 malloc 分配堆内存对象, free 手动释放),带有 GC 机制的 Go 语言在编译阶段会进行逃逸分析,自动决定将变量分配到 goroutine 的栈(stack)内存区或者全局的堆(heap)内存区上

其中的逃逸规则有很多,最简单的一种是:如果变量超出了函数调用的生命周期,编译器就会将其逃逸到堆上。

简单示例:

代码语言:javascript
复制
package main

func main() {
 A()
 B()
}

func A() int {
 a := 1024
 return a
}

func B() *int {
 b := 1024
 return &b
}

重点关注返回指针类型的 B 函数,通过 go tool compile -l -m=2 main.go 来查看逃逸结果( -l 是全局禁止函数内联,避免影响逃逸分析):

代码语言:javascript
复制
$ go tool compile -l -m=2 main.go
main.go:14:2: b escapes to heap:
main.go:14:2:   flow: ~r0 = &b:
main.go:14:2:     from &b (address-of) at main.go:15:9
main.go:14:2:     from return &b (return) at main.go:15:2
main.go:14:2: moved to heap: b

根据结果可以看出 B 函数中分配的变量 b 逃逸到了堆上(moved to heap: b),而对于全程在 A 函数生命周期内的 a 变量则没有发生逃逸(直接在栈上分配了)。

代码语言:javascript
复制
$ go tool compile -S main.go | grep runtime.newobject
        0x0020 00032 (main.go:14)       CALL    runtime.newobject(SB)
        rel 33+4 t=7 runtime.newobject+0

从汇编来看,也只有在 main.go:14 (对应源码:b := 1024 )位置处才调用了 runtime.newobject 函数。

runtime.newobject 源码

runtime.newobject 函数的作用正是执行 malloc 动作 在堆上分配内存

在栈上分配内存,将直接由 CPU 提供 push(入栈)和 pop(出栈) 指令支持,但在堆上分配,就需要额外等待 Go GC 负责回收,虽然 Go GC 十分高效,但也不可避免会造成一定的性能损耗。

所以如果想要追求极致性能,我们就要尽量避免一些不必要的堆内存分配。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-08-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 gopher云原生 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 死代码消除
  • 函数内联
  • 逃逸分析
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档