写点什么

Java 编译器优化秘籍:字节码背后的 IR 魔法与常见技巧

作者:poemyang
  • 2025-08-06
    浙江
  • 本文字数:2218 字

    阅读完需:约 7 分钟

中间表达形式

编译器通常被划分为前端编译器和后端编译器两个部分。前端编译器负责对源代码进行词法分析、语法分析和语义分析,生成中间表达形式(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;}
复制代码


未完待续


很高兴与你相遇!如果你喜欢本文内容,记得关注哦!

发布于: 刚刚阅读数: 2
用户头像

poemyang

关注

让世界知道我的存在 2018-03-05 加入

技术/人文, 互联网

评论

发布
暂无评论
Java编译器优化秘籍:字节码背后的IR魔法与常见技巧_Java虚拟机_poemyang_InfoQ写作社区