这是我参与「第五届青训营」伴学笔记创作活动的第 9 天
前言
本文主要介绍:
- 编译器和静态分析
- Go编译器优化
PS:若文章有什么问题,可以在文末进行留言或者私信。
1编译器和静态分析
1.1 编译器
-
编译器——重要的系统软件
- 编译器的作用
- 识别符合语法和非法的程序
- 生成正确且高效的代码
- 编译器的作用
-
前端 front end——分析部分
- 词法分析:生成词素(lexeme)
- 语法分析:生成语法树
- 语义分析:收集类型信息,进行语义检查
- 中间代码生成:生成intermediate representation(IR)
-
后端 back end——综合部分:
- 代码优化:生成优化后的IR,其中,这个优化与机器无关
- 代码生成:生成目标代码
-
我们主要针对于编译器后端的优化,关注点在编译器的综合部分
-
结构图:
1.2 静态分析
控制流和数据流
静态分析 :不执行程序代码,推导程序的行为,分析程序的性质。
例子:
//随便一个c的例子
int a=30
int b=9-(a/5)
int c
c=b*4
if(c>10){
c=c-10
}
return c*(60/a)
一般有两种分析:
- 控制流(Control flow):程序执行的流程
- 控制流图示:
- 数据流(Data flow):数据在控制流上的传递(基于控制流)
- 数据流图示:
通过分析控制流和数据流,我们可以知道更多关于程序的性质(properties) ,可以根据这些性质优化代码。
过程内分析和过程间分析
- 过程内分析(Intra-procedural analysis)
- 关键点:仅在函数内部进行分析
- 过程间分析(Inter-procedural analysis):
- 关键点:考虑过程调用时参数传递和返回值的数据流和控制流
我们重点关注过程间分析,因为它存在着一个比较复杂的问题:
- 举个例子:
type I interface{
bin()
}
type a struct{}
type b struct{}
func (A *a) bin(){
...
}
func (B *b) bin(){
...
}
func using(){
i=&a{}
...
i.bin() //关注这个调用
}
- 对于编译器来说,它需要通过数据流分析得知
i的具体类型,才能知道i.bin()调用的是哪个bin() - 根据
i的具体类型,产生了新的控制流,A.bin(),分析继续 - 过程间分析需要同时分析控制流和数据流——由于联合求解的缘故,因此处理起来比较复杂
2 Go 编译器优化
2.1 介绍
- 做编译器优化的原因
- 用户无感知,重新编译即可获得性能收益
- 通用性优化
- 现状
- 采用的优化少
- 编译时间较短,没有进行较复杂的代码分析和优化
- 如果要进行优化,必然会影响到编译时间
编译优化的思路:
- 场景:面向后端长期执行任务
- Tradeoff:用编译时间换取更高效的机器码
Beast mode:
- 函数内联(重点关注)
- 逃逸分析(重点关注)
- 默认栈大小调整
- 边界检查消除
- 循环展开,等等
2.2 函数内联 inlining
函数内联的定义:将被调用函数的函数体(callee)的副本替换到调用位置(caller)上,同时重写代码以反映参数的绑定
优点:
- 清除函数调用开销,例如传递参数、保存寄存器等
- 将过程间分析转化为过程内分析,帮助其他优化,例如逃逸分析
仅仅是从理论层面来说不直观
可以使用micro-benchmark验证“函数内联到底能多大程度影响性能”
// micro-benchmark的性能测试示例
func BenchmarkInline(b *testing.B) {
x := genInteger()
y := genInteger()
for i := 0; i < n.N; i++ {
addInline(x, y)
}
}
func addInline(a, b int) int {
return a + b
}
func BenchmarkInlineDisabled(b *testing.B) {
x := genInteger()
y := genInteger()
for i := 0; i < n.N; i++ {
addNoInline(x, y)
}
}
// go:noinline
func addNoInline(a, b int) int {
return a + b
}
使用上述代码进行micro-benchmark快速验证和对比性能优化后,结果如下图所示,
虽然说函数内联有着相当不错的效果,但它也是存在着缺点的。
缺点:
- 函数体变大,instruction cache,不友好(占用变大)
- 编译生成的Go镜像变大,就是用更大的空间(可能存在重复)换取时间的提升
但是,函数内联在大多数情况下是正向优化
一般来说,优化时会采取一定的内联策略,例如,考虑调用和被调用函数的规模
还有一种优秀的内联策略:Beast Mode方案——一种时间和空间的折中方案
- Go函数内联受到的限制较多
- 语言特性,例如interface,defer等,限制了函数内联
- 内联策略非常保守
- Beast mode:调整函数内联的策略,使更多函数被内联
- 降低函数调用的开销
- 增加了其他优化机会:逃逸分析
- 开销:
- Go镜像增加约10%
- 编译时间增加
2.3 逃逸分析
逃逸分析的定义:分析代码中指针的动态作用域:指针在何处可以被访问
大致思路
-
观察对象的数据流从对象分配处出发,沿着控制流,
-
若发现指针 p 在当前作用域 s:
- 作为参数传递给其他函数
- 传递给全局变量
- 传递给其他的 goroutine(协程)
- 传递给已逃逸的指针指向的对象
-
则指针 p 指向的对象逃逸出 s,反之则没有逃逸出s
-
对于之前提到的Beast mode
- Beast mode使得函数内联拓展了函数边界,更多对象不逃逸
-
优化: 未逃逸的对象可以在栈上分配
- 对象在栈上分配和回收很快: 移动 sp
- 减少在 heap 上的分配,降低 GC 负担
利用到Beast Mode方案,其性能收益主要在函数内联和逃逸分析的检测上,Beast Mode方案带来的优化能够得到令人满意的效果。
心得
通过课程的学习,我重温了编译器相关知识,了解了静态分析的本质,对控制流与数据流有了充分的认识,也理解了何为过程内分析和何为过程间分析,而对于我学习go语言来说,关键还是在于go编译器的优化。通过对前置知识的学习以及逐步学会分析go编译器的优化方向,能够很好的理解课程提出来的Beast mode方案,理解其优化的意义,获益匪浅!
引用
ppt:高性能 Go 语言发行版优化与落地实践 .pptx - 飞书云文档 (feishu.cn)