这是我参与「第五届青训营」伴学笔记创作活动的第 6 天,今天学习的内容是关于编译器和静态分析、Go编译器优化,根据课程内容整理学习笔记如下。
5.4 编译器和静态分析
5.4.1 编译器的结构
-
重要的系统软件
-
分析部分(前端 front end)
- 词法分析,生成词素(lexeme)
- 语法分析,生成语法树
- 语义分析,收集类型信息,进行语义检查
- 中间代码生成,生成 intermediate representation(IR)
-
综合部分(后端 back end)
- 代码优化,机器无关优化,生成优化后的IR
- 代码生成,生成目标代码
5.4.2 静态分析
-
静态分析:不执行程序代码,推导程序的行为,分析程序的性质。
-
控制流(Control flow): 程序的执行流程
上图的程序转换成控制流图 (control-flow graph)
-
数据流(Data flow): 数据在控制流上的传递
-
通过分析控制流和数据流,我们可以知道更多关于程序的性质(properties) ,这些事实可以帮助我们做编译优化。
- 例如上面的程序。我们通过分析数据流和控制流,知道这个程序始终返回 4。编译器可以根据这个结果做出优化。
5.4.3 过程内分析与过程间分析
- 过程内分析/函数内分析(Intra-procedural analysis):在函数内进行控制流和数据流的分析
- 过程间分析/函数间分析(Inter-procedural analysis):除了函数内的分析,还需要考虑跨函数(函数调用时参数传递和返回值等) 的数据流和控制流(复杂,原因在如同名函数复用等问题,需要进行联合求解)
5.5 Go 编译器优化
5.5.1 概述
-
目的
- 用户无感知,重新编译即可获得性能收益
- 通用的优化手段
-
现状
- 采用的优化较少
- 追求编译时间短,因此没有进行复杂的代码分析和优化
-
思路
- 面向后端长期执行的任务
-
Tradeoff: 用适当增加编译时间换取更高性能的机器码
-
Beast mode
- 函数内联
- 逃逸分析
- 默认栈大小调整
- 边界检查消除
- 循环展开
- ......
5.5.2 函数内联 Inlining
-
定义: 将被调用函数的函数体(callee)的副本替换到调用位置(caller)上,同时重写代码以反映参数的绑定
-
优点
- 消除函数调用开销,例如传递参数、保存寄存器等
- 将过程间分析的问题转换为过程内分析,帮助其他分析(如逃逸分析)
-
缺点
- 函数体变大,instruction cache(icache)不友好
- 编译生成的 Go 镜像文件变大
-
函数内联在大多数情况下是正向优化,即多内联,会提升性能
-
采取一定的策略决定是否内联
- 调用和被调用函数的规模
5.5.3 Beast Mode
-
Go 内联的限制较多
- 语言特性:interface, defer 等等,限制了内联优化
- 内联策略非常保守
-
字节跳动的优化方案Beast mode:修改了内联策略,让更多函数被内联
- 降低了函数调用的开销
- 增加了其他优化的机会:逃逸分析
-
开销
- Go 镜像大小略有增加(~10%)
- 编译时间增加
- 运行时栈扩展开销增加
5.5.4 逃逸分析
-
定义:分析代码中指针的动态作用域,即指针在何处可以被访问
-
大致思路
-
从对象分配处出发,沿着控制流,观察数据流
-
若发现指针 p 在当前作用域 s:
- 作为参数传递给其他函数;
- 传递给全局变量;
- 传递给其他的 goroutine;
- 传递给已逃逸的指针指向的对象;
-
则指针 p 逃逸出 s,反之则没有逃逸出 s.
-
-
Beast mode:函数内联拓展了函数边界,更多对象不逃逸
-
优化: 未逃逸出当前函数的指针指向的对象可以在栈上分配
- 对象在栈上分配和回收很快:移动 sp 即可完成内存的分配和回收
- 减少在堆上分配对象,降低 GC 负担