这是我参与「第五届青训营 」伴学笔记创作活动的第 5 天
编译器和静态分析
编译器结构
编译器是一种重要的系统软件,它的作用有:识别符合语法和非法的程序,针对不同的平台(arm64、amd64、...)生成正确且高效的代码
编译器的前后端可细化分为如下模块:
它可以简化为两个部分:分析部分和综合部分,他是一个顺序的结构
分析部分(前端 frontend)
- 词法分析,这部分将会生成词素(lexeme)
- 语法分析,把词素整合成抽象语法树(AST)
- 语义分析,(在语法树上)搜集类型信息,进行语义检查,把语义信息放到AST上,所以语义分析生成的结果又叫做decorated AST(装饰过的AST)
- 中间代码生成,根据AST生成IR(Intermediate Representation),这个IR是与机器无关的,也就是说不论是arm64、amd64还是什么,IR是一样的
综合部分(后端 backend)
- 代码优化,机器无关优化,生成优化后的IR
- 代码生成,生成目标代码
静态分析
静态分析指的是不执行程序代码,推导程序的行为,分析程序的性质
一般来说,最常见的分析有两种:控制流和数据流分析
数据流分析和控制流分析
- 控制流(Control flow),分析程序执行的流程(通常会用Control-Flow Graph表示)
- 数据流(Data flow),数据在控制流上的传递
我们通过分析控制流和数据流,可以知道更多关于程序的性质(properties),并且根据这些性质来优化代码
也可以理解成人肉编译优化
过程内分析和过程间分析
过程内分析(Intra-procedural analysis),只在函数内部进行分析
过程间分析(Inter-procedural analysis),考虑过程调用时参数传递和返回值的数据流和控制流
过程内分析其实是蛮简单的,因为限定范围在了单个函数内,过程间分析反而是比较难的,具体如下:
- 需要通过数据流分析得知某些变量的具体类型,我们才可以知道某个函数调用的具体是哪个函数
- 我们根据变量的具体类型,也产生了新的控制流,我们需要扩大分析范围
- 过程间分析需要同时分析控制流和数据流,相当于联合求解,难度是比较大的
Golang编译器优化
基本介绍
为什么我们要做编译器优化?
用户无需感知,重新编译即可获得性能收益,同时具有一定的通用普适性
编译器优化现状
目前来说,golang编译器采用的优化少,编译时间短,没有进行较复杂的代码分析和优化
主要原因是go的编译思路是想缩短编译时间
字节的编译器优化的思路
bytedance面向的场景业务是后端长期执行的任务,因此可以进行适当的取舍(tradeoff):用编译时间换取更高效的机器码,字节自己有Beast Mode,这个产品具有函数内敛、逃逸分析、默认栈大小的调整、边界检查消除、循环展开等等
函数内联(Inlining)
内敛:将被调用函数的函数体(callee)的副本替换到调用位置(caller)上,同时重写代码以反映参数的绑定
在c++中可以使用linline关键字指导编译器内联
函数内联具有如下优点:
- 消除函数调用开销,例如传参和保存寄存器
- 把过程间分析转化为过程内分析,帮助其他优化,例如逃逸分析
通过Micro benchmark分析一个a+b函数的内联,可以达到4.5倍左右的提升(参考机器CPU是i7-1068NG7)
但是函数内联也是有缺点的
- 函数体变大,instruction cache(icache)不友好(不好把函数代码丢进去),就会频繁的icache miss
- 编译生成的Go镜像也会变大(但是其实可以忽略,毕竟硬盘成本较低)
在大多数情况下,函数内联通常都是正向优化,但是并不是所有函数都适合内联,比如这个函数是个大递归,或者说这个函数体积非常大
因此编译器也需要一个自己的内联策略,比如调整被调用函数的规模
例如,根据caller和callee的大小来判断,比如caller已经很大了,那么我们就不把callee内联进来
Beast Mode
这是Bytedance在Go SDK上开发的编译器优化产品
首先,Go的函数内联受到的限制是比较多的,比如语言特性,interface,defer等,限制了函数内联,同时golang的编译器内联策略非常的保守,大多数函数并不会被内联
因此,Beast Mode调整了内联的策略,让更多的函数被内联系,这带来了两个好处
- 降低函数调用的开销
- 增加了其他优化的机会:例如逃逸分析
但是作为取舍(tradeoff),我们增加了go的镜像大小(增加约10%),同时编译时间增加
逃逸分析
逃逸分析指的是,分析代码中指针的动态作用域,也就是这个指针何时会被访问到
逃逸分析的大致思路是这样的
-
从对象分配出发,沿着控制流,观察对象的数据流
-
若发现指针p在当前作用域s:
- 作为参数传递给其他函数
- 传递给全局变量
- 传递给其他的goroutine
- 传递给已逃逸的指针指向的对象
-
则指针p指向的对象逃逸出s,否则就没有逃逸出s
Beast Mode对逃逸分析的优化:函数内联拓展了函数边界,让更多的对象不逃逸
未逃逸的对象可以在栈上分配:
- 对象在栈上分配和回收很快:只需要移动sp
- 同时减少了在heap上的分配,降低GC负担
开了Beast Mode后,高峰期CPU Usage降低9%,时延降低10%,内存使用降低3%
参考
知乎 - 编译器结构:zhuanlan.zhihu.com/p/148054386