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