中间表达形式
编译器通常被划分为前端编译器和后端编译器两个部分。前端编译器负责对源代码进行词法分析、语法分析和语义分析,生成中间表达形式(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)用于消除冗余的计算。编译器通过跟踪每个计算的值,如果发现两个计算的值相同,就可以将其中一个计算替换为另一个计算的结果。
// 值编号前的代码
int a = 5;
int b = 10;
int c = a + b;
int d = a + b;
// 值编号后的代码
int a = 5;
int b = 10;
int c = a + b;
int d = c;
常数折叠
常量折叠(Constant folding)通过在编译时计算常数表达式的值,将这些表达式替换为它们的计算结果。
// 常量折叠前的代码
int a = 5 * 10;
// 常量折叠后的代码
int a = 50;
常数传播
常数传播(Constant oropagation)它通过分析代码中的常数赋值和使用,将常数值直接传播到使用它们的表达式中。
// 常量传播前的代码
int a = 10;
int b = 20;
int c = a + b;
// 常量传播后的代码
int c = 10 + 20;
死代码消除
死代码消除(Dead Code Elimination)旨在移除程序中不会影响最终结果的代码,减少程序的大小。
// 死代码消除前的代码
int a = 10;
int b = 20;
int c = a + b; // 这行代码是死代码,因为 c 没有被使用
if (a > 5) {
print("a is greater than 5");
}
int d = 30; // 这行代码是死代码,因为 d 没有被使用
// 死代码消除后的代码
int a = 10;
if (a > 5) {
print("a is greater than 5");
}
公共子表达式消除
公共子表达式消除(Common subexpression elimination,CSE)通过识别并消除重复的子表达式,避免在运行时多次计算相同的子表达式。
// 公共子表达式消除前的代码
int a = x * y;
int b = x * y;
// 公共子表达式消除后的代码
int a = x * y;
int b = a;
null判断消除
null判断消除(Null check elimination)通过在编译时分析代码,确定某些引用不可能为null,从而消除不必要的null检查。
// null判断消除前的代码
String str = "Hello, World!";
// Null检查
if (str != null) {
System.out.println(str);
}
// null判断消除后的代码
// 编译器可以确定str不可能为null,从而消除null检查
String str = "Hello, World!";
System.out.println(str);
边界检查消除
边界检查消除(Bounds check elimination)通过在编译时分析代码,判断数组访问是否越界,从而在运行时避免不必要的边界检查。
// 边界检查消除前的代码
// for循环中的数组访问array[i]需要进行边界检查
int[] array = new int[10];
for (int i = 0; i < array.length; i++) {
array[i] = i * 2;
}
// 边界检查消除后的代码
// 编译器消除了for循环中的边界检查,因为它可以在编译时确定i的值不会越界
int[] array = new int[10];
for (int i = 0; i < 10; i++) {
array[i] = i * 2;
}
循环展开
循环展开(Loop unrolling)通过减少循环次数并在每次循环中执行更多的操作,以减少循环控制开销。同时它还会增加一个基本块中的指令数量,从而为指令排序的优化算法创造机会。在循环展开的基础上,可以实现把多次计算优化成一个向量计算。
// 循环展开前的代码
int sum = 0;
for (int i = 1; i <= 10; i++) {
sum += i;
}
// 循环展开后的代码
// 优化后将循环次数减少到了5次
int sum = 0;
for (int i = 1; i <= 10; i += 2) {
sum += i;
sum += i + 1;
}
未完待续
很高兴与你相遇!如果你喜欢本文内容,记得关注哦!