Chapter 11.3-11.4 - 提前编译器 && 编译器优化技术
Created by : Mr Dk.
2020 / 02 / 02 22:29 🧨🧧
Ningbo, Zhejiang, China
11.3 提前编译器
11.3.1 提前编译的优劣得失
提前编译 (Ahead Of Time, AOT) 在 Java 中的价值直指即时编译的最大弱点:即时编译需要占用运行时资源。比如,编译时最耗时的优化措施是进行 过程间分析,必须在全程序范围内做大量耗时计算工作。目前常见的 JVM 都是通过大规模的函数内联 + 过程内分析来模拟过程间分析。提前编译的本质是给即时编译器做缓存加速,改善 Java 启动后需要一段时间的预热才能到达最高性能的问题。
提前编译不仅要和目标机器相关,还需要与 JVM 运行时的参数绑定。即时编译器相对于提前编译器的天然优势:
- 性能分析制导优化 (Profile-Guided Optimization, PGO)
- 解释器和客户端编译器会不断收集性能监控信息
- 这些信息在静态分析时是无法得到的
- 动态运行时可以看出其偏好性
- 激进预测性优化 (Aggressive Speculative Optimization)
- 性能监控信息能够支持一些正确可能性很大的预测,就可以按高概率的假设进行优化
- 万一真的出现了错误预测,大不了退回到低级编译器甚至解释器上执行
- 只要出错的概率足够低,这样的优化就能大幅降低目标复杂度
- 链接时优化 (Link-Time Optimization, LTO)
- Java 语言天生就是动态链接的
- C/C++ 的主程序与动态链接库在编译时完全独立,不能联合优化
11.4 编译器优化技术
11.4.2 方法内联
把目标代码原封不动地复制到发起调用的代码中,避免发生真实的函数调用。JVM 引入了 类型继承关系分析 (Class Hierarchy Analysis, CHA) 技术,确定类型之间的关系
- 对于非虚函数,可以直接内联
- 对于虚函数
- 如果 CHA 只查询到只有一个目标版本,则进行 守护内联 (Guarded Inlining)
- 由于 Java 的动态链接性,如果有新的类型加载,改变了 CHA 的结论,则发生 激进预测性优化,需要放弃已经编译的代码,回退到解释器或重新编译
- 如果 CHA 确认有多个版本的目标函数可供选择,使用内联缓存来缩减函数调用开销
- 缓存建立在目标函数的入口之前
- 缓存记录函数接收者的类型,并在每次函数调用时都比较接收者的版本
- 如果每次接收者都是一样的,则是单态内联缓存
- 如果出现了接收者不一致,则退化为超多态内联缓存,相当于查虚函数表进行函数分派
11.4.3 逃逸分析 (Escape Analysis)
分析对象的动态作用域,划分逃逸程度:
- 从不逃逸
- 函数逃逸 - 对象在函数中被定义后,可能作为参数被外部函数引用
- 线程逃逸 - 对象会被外部线程访问到
如果一个对象逃逸程度较低,就可能进行不同程度的优化:
- 栈上分配 (Stack Allocation)
- 确定一个对象不会逃逸出线程,那么可以在栈上为其分配内存,而不是在 Java Heap 上
- 对象占用的内存随栈帧出栈而被销毁
- 从不逃逸和函数逃逸的对象占比很高,大量对象会随着函数结束而自动销毁,GC 子系统压力下降
- 不支持线程逃逸
- 标量替换 (Scalar Replacement)
- 标量指不可再分为更小数据的数据,比如原始数据类型
- 将对象用到的成员变量恢复为原始类型来访问
- 程序执行时可以不创建对象,只创建使用到的成员即可
- 栈上分配 + 后续优化
- 不支持函数逃逸
- 同步消除 (Synchronization Elimination)
- 如果确定变量不会线程逃逸,对变量的所有同步措施可以安全地消除掉
逃逸计算的成本非常高,且不能保证逃逸分析带来的性能收益会高于消耗。所以 JVM 中只能采用不那么准确,但时间压力较小的算法。逃逸分析可能出现效果不稳定的情况,或分析过程耗时却无法辨别出非逃逸对象,导致性能下降。
11.4.4 公共子表达式消除
不重复计算已经计算过的表达式。
11.4.5 数组边界检查消除
在 Java 中会对数组访问自动进行上下界的范围检查,因此每次数组元素的读写都带有一次隐含的条件判定操作。对于拥有大量数组访问的程序,是一种性能负担。对于可以确定合法范围的数组访问,尽可能将运行期检查提前到编译期内完成,可以省掉数组的上下界检查。