这是我参与「第五届青训营 」伴学笔记创作活动的第 7 天
编译器和静态分析
编译器的结构
-
编译器是重要的系统软件
- 识别符合语法和非法的程序
- 生成正确且高效的代码
-
分析部分(前端 front end)
-
词法分析,生成词素(lexeme)
-
语法分析,生成语法树
-
语义分析,收集类型信息,进行语义检查
-
中间代码生成,生成 intermediate representation (IR)
- 注意:IR 是机器无关的,即无论是 x86 还是 amd86都能正常运行
-
-
综合部分(后端 back end)
- 代码优化,机器无关优化,生成优化后的IR
- 代码生成,生成目标代码
静态分析
-
概念:不执行代码,推导程序的行为,分析程序的性质
-
以一段简单的代码为例
//init int a = 30 int b = 9 - (a / 5) int c c = b * 4 //server if (c > 10) { c = c - 10 } //response return c * (60 / a) -
控制流 (Control flow):程序执行的流程
-
数据流 (Data flow):数据在控制流上的传递
int a = 30 int b = 3 int c c = 12 if (true) { c = 2 } // c = 2 return c * 2 // return 4 -
通过分析控制流和数据流,我们可以知道更多关于程序的性质 (properties)
- 根据这些性质优化代码
过程内分析和过程间分析
过程内分析
- 仅在过程内部进行分析
过程间分析
- 考虑过程调用时参数传递和返回值的数据流和控制流
- 一般情况,过程间分析需要同时分析控制流和数据流
Go 编译器优化
-
编译器优化的好处
- 用户无感知,重新编译即可获得性能收益
- 通用性优化
-
现状
- Go 的官方编译器采用的优化少
- 由于编译时间较短,没有进行较为复杂的代码分析和优化
-
优化思路
- 场景:面向后端的长期执行任务
- Tradeoff:用编译时间换取更高效的机器码
-
Beast mode
- 函数内联
- 逃逸分析
- 默认栈大小调整
- 边界检查消除
- 循环展开
- ……
函数内联 (Inlining)
-
概念:将被调用函数的函数体 (callee) 的副本替换到调用位置上 (caller) 上,同时重写代码以反映参数的绑定
-
优点:
- 消除函数调用的开销,例如传递参数、保持寄存器等
- 将过程间分析转换成过程内分析,可以帮助其他优化,例如 “逃逸分析”
-
性能测试:micro-benchmark
-
缺点:
-
函数体变大,对于 instruction cache (icache) 不友好
- 容易出现 icache miss
-
编译生成的 Go 镜像变大
-
-
函数内联在大多数情况下是正优化
-
内联策略
- 调用和被调用的规模
- ……
Beast Mode
-
Go 函数内联受到的限制较多
- 语言特性,例如 interface, defer 等,限制了函数内联
- 内联策略非常保守
-
Beast Mode:调整函数调用的策略,使更多函数内联
- 降低了函数调用的开销
- 增加了其他优化的机会
-
开销
- Go 镜像增加 ~10%
- 编译时间增加
逃逸分析
-
概念:分析代码中指针的动态作用域 => 指针在何处可以被访问
-
大致思路:
-
从对象分配处出发,沿着控制流,观察对象的数据流
-
若发现指针p在当前作用域s:
- 作为参数传递给其他函数
- 传递给全局变量
- 传递给其他的 goroutine
- 传递给已逃逸的指针指向的对象
-
则指针p指向的对象逃逸出s,反之则没有逃逸出s
-
-
Beast Mode:函数内联拓展了函数的边界,更多对象不逃逸
-
优化:未逃逸的对象可以在栈上分配
- 对象在栈上分配和回收很快:移动 sp
- 减少在 heap 上的分配,降低 GC 负担