这是我参与「第五届青训营」笔记创作活动的第十二天。
一、本堂课的重点内容
二、详细知识点如下
1、编译器的结构
- 静态分析:不执行代码,推导程序的行为,分析程序的性质。
- 控制流:程序的执行流程
- 数据流:数据在控制流上的传递
上图的程序转换成控制流图 (control-flow graph)
通过分析控制流和数据流,我们可以知道更多关于程序的性质(properties) ,这些事实可以帮助我们做编译优化。
- 例如下面的程序。我们通过分析数据流和控制流,知道这个程序始终返回 4。编译器可以根据这个结果做出优化。
- Intra-procedural analysis: 函数内分析:在函数内进行控制流和数据流的分析。
- Inter-procedural analysis: 函数间分析:除了函数内的分析,还需要考虑跨函数的数据流和控制流,例如参数传递,函数返回值等。
2、Go 编译器优化
1)目的
- 用户无感知,重新编译即可获得性能收益
- 通用的优化手段
2) 现状
- 采用的优化较少
- 追求编译时间短,因此没有进行复杂的代码分析和优化
3)思路
- 面向后端长期执行的任务
- 用适当增加编译时间换取更高性能的代码
4) 函数内联
- 定义:将被调用函数的函数体的副本替换到调用位置上,同时重写代码以反映参数的绑定
-
优点
- 消除调用开销
- 将过程间分析的问题转换为过程内分析,帮助其他分析,例如逃逸分析。
使用micro-benchmark快速验证和对比性能优化结果。
-
缺点
- 函数体变大
- 编译生成的 Go 镜像文件变大
函数内联在大多数情况下是正向优化,即多内联,会提升性能。
5)Beast Mode
-
Go 内联的限制
- 语言特性:interface, defer 等等,限制了内联优化
- 内联策略非常保守
-
字节跳动的优化方案
- 修改了内联策略,让更多函数被内联
- 增加了其他优化的机会:逃逸分析
-
开销
- Go 镜像大小略有增加
- 编译时间增加
- 运行时栈扩展开销增加
6)逃逸分析
- 定义:分析代码中指针的动态作用域,即指针在何处可以被访问
-
大致思路
从对象分配处出发,沿着控制流,观察数据流。若发现指针 p 在当前作用域 s:
- 作为参数传递给其他函数; - 传递给全局变量; - 传递给其他的 goroutine; - 传递给已逃逸的指针指向的对象;- 则指针 p 逃逸出 s,反之则没有逃逸出 s.
函数内联拓展了函数边界,更多对象不逃逸。
优化:未逃逸出当前函数的指针指向的对象可以在栈上分配
对象在栈上分配和回收很快:移动 sp 即可完成内存的分配和回收;
减少在堆上分配对象,降低 GC 负担。
三、课后个人总结
编译器 Beast mode 拥有更多的优化手段,执行效率更高。在开发阶段使用标准编译模式,提高开发效率;发布到线上时使用 Beast mode 编译生成性能更高的二进制。
这个编译器优化效果比较明显,在很多 Benchmark 上都取得了比较好的效果,如下图所示,time/op 越少越好.