Java 编译器优化秘籍:字节码背后的 IR 魔法与常见技巧
中间表达形式
编译器通常被划分为前端编译器和后端编译器两个部分。前端编译器负责对源代码进行词法分析、语法分析和语义分析,生成中间表达形式(Intermediate Representation ,IR)。这种由前端生成的 IR 被称为高级中间表达形式(High Intermediate Representation,HIR),其优化主要与源代码本身的特性有关。后端后端编译器则将 HIR 转换为低级中间表达形式(Low Intermediate Representation,LIR),并进行进一步的优化。这种优化主要与目标机器的硬件特性有关。最终,LIR 会被翻译成目标机器代码。在不考虑解释执行的情况下,源代码到最终机器码的转换过程通常包括两个编译阶段:1)Java 编译器(如 javac)将源代码编译成字节码;2)即时编译器将字节码编译成机器码。对于即时编译器,字节码直接被视为一种 IR,是 Java 虚拟机的通用“语言”。它结构化、平台无关,但对于进行复杂的全局优化而言,其基于栈的指令格式和较为紧凑的表示方式并不总是最理想的。因此,当即时编译器(如 C1 或 C2)开始工作时,它首先会将输入的字节码转换为其内部更适合分析和优化的 IR。现代编译器通常采用图结构的 IR,其中静态单赋值(Static Single Assignment,SSA)IR 是一种常用的 IR 特性,它要求每个变量只被赋值一次。SSA 形式极大地简化了许多优化算法的实现,如常量传播、死代码消除、公共子表达式消除、寄存器分配等。HotSpot C2 编译器采用一种称为 Sea of Nodes(节点之海)的高度优化的图 IR,它就是基于 SSA 的。这种 IR 将程序表示为数据流图和控制流图的结合,节点代表操作,边代表数据流或控制依赖。它允许进行非常自由和强大的代码变换和优化。总结来说,从 Java 源代码到最终在处理器上执行的机器码,如果排除纯解释执行,大致经历以下 IR 转换:源代码 ->(javac)->Java 字节码 ->(即时编译前端) ->即时编译内部 HIR (如 SSA 图) ->(即时编译中端优化) ->即时编译内部 LIR ->(即时编译后端) ->目标机器码。

机器无关的编译优化
编译优化的方法主要可以分为机器无关与机器相关的优化。1)机器无关的优化与硬件特征无关,比如把常数值在编译期计算出来(常数折叠)。2)机器相关的优化则需要利用某硬件特有的特征,比如 SIMD 指令等。
值编号
值编号(Value numbering)用于消除冗余的计算。编译器通过跟踪每个计算的值,如果发现两个计算的值相同,就可以将其中一个计算替换为另一个计算的结果。
常数折叠
常量折叠(Constant folding)通过在编译时计算常数表达式的值,将这些表达式替换为它们的计算结果。
常数传播
常数传播(Constant oropagation)它通过分析代码中的常数赋值和使用,将常数值直接传播到使用它们的表达式中。
死代码消除
死代码消除(Dead Code Elimination)旨在移除程序中不会影响最终结果的代码,减少程序的大小。
公共子表达式消除
公共子表达式消除(Common subexpression elimination,CSE)通过识别并消除重复的子表达式,避免在运行时多次计算相同的子表达式。
null 判断消除
null 判断消除(Null check elimination)通过在编译时分析代码,确定某些引用不可能为 null,从而消除不必要的 null 检查。
边界检查消除
边界检查消除(Bounds check elimination)通过在编译时分析代码,判断数组访问是否越界,从而在运行时避免不必要的边界检查。
循环展开
循环展开(Loop unrolling)通过减少循环次数并在每次循环中执行更多的操作,以减少循环控制开销。同时它还会增加一个基本块中的指令数量,从而为指令排序的优化算法创造机会。在循环展开的基础上,可以实现把多次计算优化成一个向量计算。
未完待续
很高兴与你相遇!如果你喜欢本文内容,记得关注哦!
版权声明: 本文为 InfoQ 作者【poemyang】的原创文章。
原文链接:【http://xie.infoq.cn/article/fbcb585abf8ef8e5cadcc32dc】。文章转载请联系作者。
评论