文章目录
- 1. 历史背景
- 2. 构建编译调试环境
- 3. JVMCI 编译器接口
- 4. 代码中间表示
- 5. 代码优化与生成
1. 历史背景
Graal 编译器在 JDK 9 以 Jaotc
提前编译工具的形式首次加入到官方的 JDK 中,JDK 10 开始提供替换(得益于 HotSpot 编译器接口,Java Level JVM Compiler Interface
,JVMCI
)服务端编译器支持;
JVMCI 的三个功能
- 响应 HotSpot 的编译请求,并将请求分发给 Java 实现的即时编译器;
- 允许编译器访问 HotSpot 中与即时编译相关的数据结构,包括类、字段、方法及其性能监控数据等(提供这些数据结构在 Java 语言层面的抽象表示);
- 提供 HotSpot 代码缓存(Code Cache)的 Java 端抽象表示,允许编译器部署编译完成的二进制机器码;
JVMCI 可以把一个 HotSpot VM 外部的、用 Java 语言实现的即时编译器(不限于 Graal)集成到 HotSpot 中,响应 HotSpot 发出的最顶层的编译请求,并将编译后的二进制代码部署到 HotSpot 的代码缓存;又绕开 HotSpot 的即时编译系统,让编译器直接为应用类库编译出二进制机器码(可当作提前编译器使用,如 Jaotc);
2. 构建编译调试环境
- 安装 Graal VM 构建工具 mx
git clone https://github.com/graalvm/mx.git
export PATH=`pwd`/mx:$PATH
- 安装带有 JVMCI 的 OpenJDK8
export JAVA_HOME=~/devtools/oraclejdk1.8.0_212-jvmci-20-b01
- 获取 Graal 编译器代码
git clone https://github.com/graalvm/graal.git
- 使用 mx 构建 Graal 编译器
cd graal/compiler
mx build
- 使用 mx 创建项目
cd graal/compiler
mx eclipseinit
编译构建项目可能需要 2GB 已上内存,因此需要调大 IDE 的堆最大内存;
需将带有 JVMCI 的 JDK 作为 IDE 项目的编译运行使用的版本;
3. JVMCI 编译器接口
interface JVMCICompiler {// 编译的输入是字节数组表示的字节码;额外携带各种方法相关信息,如局部变量表中的变量槽个数、操作数栈的最大深度、分层编译收集到的统计信息等;// 编译输出的也是字节数组表示的二进制机器码;void compileMethod(CompilationRequest request);
}interface CompilationRequest {JavaMethod getMethod();
}interface JavaMethod {byte[] getCode();int getMaxLocals();int getMaxStackSize();ProfilingInfo getProfilingInfo();... // 省略其他方法
}
即时编译演示
// 示例代码
public class Demo {public static void main(String[] args) {// 循环构成热点代码触发即时编译while (true) {workload(14, 2);}}private static int workload(int a, int b) {return a + b;}
}
# 编译演示
$ javac Demo.java
$ java \-XX:+PrintCompilation \-XX:CompileOnly=Demo::workload \Demo
...193 1 3 Demo::workload (4 bytes)199 2 1 Demo::workload (4 bytes)199 1 3 Demo::workload (4 bytes) made not entrant
...
wordload() 方法被分层编译多次,made not entrant
表示方法被丢弃;
限制性编译演示
# JDK 8 的运行配置
-Djvmci.class.path.append=~/graal/compiler/mxbuild/dists/jdk1.8/graal.jar:~/graal/sdk/mxbuild/dists/jdk1.8/graal-sdk.jar
-XX:+UnlockExperimentalVMOptions
-XX:+EnableJVMCI
-XX:+UseJVMCICompiler
-XX:-TieredCompilation
-XX:+PrintCompilation
-XX:CompileOnly=Demo::workload# JDK 9 或以上版本的运行配置
--module-path=~/graal/sdk/mxbuild/dists/jdk11/graal.jar
--upgrade-module-path=~graal/compiler/mxbuild/dists/jdk11/jdk.internal.vm.compiler.jar
-XX:+UnlockExperimentalVMOptions
-XX:+EnableJVMCI
-XX:+UseJVMCICompiler
-XX:-TieredCompilation
-XX:+PrintCompilation
-XX:CompileOnly=Demo::workload
97 1 Demo::workload (4 bytes)
HotSpotGraalCompiler
类中的 compileMethod()
方法实际实现了编译过程;
参数解释
-XX:+UnlockExperimentalVMOptions
,解锁实验性特性;-XX:+EnableJVMCI
,启用 JVMCI 接口;-XX:+UseJVMCICompiler
,启用 JVMCI 编译器;-XX:-TieredCompilation
,关闭分层编译;-XX:+PrintCompilation
,打印即时编译过的方法;-XX:CompileOnly=Demo::workload
,限定只允许编译 workload() 方法;
4. 代码中间表示
Graal 编译器与 HotSpot C2 编译器保持一致的中间表示形式:Sea-of-Nodes
,Ideal Graph
,Structured Graph
,一种程序依赖图
形式(Program Dependence Graph
,PDG);
理想图
(Ideal Graph
),一种有向图,其节点表示程序中的元素(变量,操作符、方法、字段等),其变表示数据流(虚线)或控制流(实线);
x+y 的理想图
x、y 两个节点的数据流流入相加操作
节点,相加结果数据流出;
getX() + getY() 的理想图
先调佣 getX(),再调用 getY();
-Dgraal.Dump
,输出 Graal 编译器构造的理想图;可通过mx igv
命令获得相应Ideal Graph Visualizer
工具;
(a + b) / 2 的理想图
int average(int a, int b) {return (a + b) / 2;
}
- P(0)、P(1) 表示参数 0、1,流入
相加操作
节点; - C(2) 表示常量 2,与相加结果一起流入
除法操作
节点;
公共子表达式消除演示
// 公共子表达式能够被消除示例
int workload(int a, int b) {return (a + b) * (a + b);
}// 公共子表达式是不可以被消除示例
int workload() {return (getA() + getB()) * (getA() + getB());
}
- 公共子表达式能够被消除的理想图
参数 0、1 的加法操作只进行了一次,却流出了两条数据给乘法操作;
- 公共子表达式是不可以被消除的理想图
四次方法调用全部执行,两个加法操作在两个独立节点进行,该版本不会进行公共子表达式消除;
5. 代码优化与生成
Graal 编译器通过 greateGraph() 方法将字节码转成理想图;
- 理想图是一组不为空的节点集合,所有节点都是继承自 ValueNode 类型的不同子类型(如 AddNode 表示加法操作,BinaryArithmeticNode 表示二院算术操作、BinaryNode 表示二元操作);
- 字节码到理想图的过程与栈帧中操作数与指令的操作规则相对应(在 BytecodeParser 类中实现,BytecodeParser::getArithmeticOp() 可以看到 iadd 操作码的实现);
getIntegerAdd() 创建 AddNode 节点
protected ValueNode genIntegerAdd(ValueNode x, ValueNode y) {return AddNode.create(x, y, NodeView.DEFAULT);
}
理想图节点的主要操作
规范化
(Canonicalisation
),缩减理想图的规模,优化代码;生成机器码
(Generation
),代码翻译,Graal 不直接讲理想图转换成机器码,而是先生成 LIR(机器指令集相关),再交由 HotSpot 统一后端产生机器码;Graal 编译器支持的指令集平台只有(SPARC、x86-AMD64、ARMv8-AArch64);
AddNode 的创建实现
public static ValueNode create(ValueNode x, ValueNode y, NodeView view) {BinaryOp<Add> op = ArithmeticOpTable.forStamp(x.stamp(view)).getAdd();Stamp stamp = op.foldStamp(x.stamp(view), y.stamp(view));ConstantNode tryConstantFold = tryConstantFold(op, x, y, stamp, view);if (tryConstantFold != null) {return tryConstantFold;}if (x.isConstant() && !y.isConstant()) {return canonical(null, op, y, x, view);} else {return canonical(null, op, x, y, view);}
}
- canonical() 实现节点的规范化(
算术聚合
、符号合并
等);全局规范化则在 CanonicalizerPhase::tryGlobalValueNumbering() 中实现; - tryConstantFold() 尝试常量折叠;
- AddNode::generate() 进行机器码生成;
编译修改演示
class AddNode {void generate(...) {// 将原来的 emitAdd() 替换成 emitSub()... gen.emitSub(op1, op2, false) ...}
}
不修改 AddNode::generate() 的汇编代码
0x000000010f71cda0: nopl 0x0(%rax,%rax,1)
0x000000010f71cda5: add %edx,%esi ;*iadd {reexecute=0 rethrow=0 return_oop=0}; - Demo::workload@2 (line 10)0x000000010f71cda7: mov %esi,%eax ;*ireturn {reexecute=0 rethrow=0 return_oop=0}; - Demo::workload@3 (line 10)0x000000010f71cda9: test %eax,-0xcba8da9(%rip) # 0x0000000102b74006; {poll_return}
0x000000010f71cdaf: vzeroupper
0x000000010f71cdb2: retq
修改 AddNode::generate() 的汇编代码
0x0000000107f451a0: nopl 0x0(%rax,%rax,1)
0x0000000107f451a5: sub %edx,%esi ;*iadd {reexecute=0 rethrow=0 return_oop=0}; - Demo::workload@2 (line 10)0x0000000107f451a7: mov %esi,%eax ;*ireturn {reexecute=0 rethrow=0 return_oop=0}; - Demo::workload@3 (line 10)0x0000000107f451a9: test %eax,-0x1db81a9(%rip) # 0x000000010618d006; {poll_return}
0x0000000107f451af: vzeroupper
0x0000000107f451b2: retq
0x000000010f71cda5: add
指令变为 0x0000000107f451a5: sub
;
Graal 编译器的出现对学习和研究虚拟机代码编译技术提供了巨大的便利,让 Java 开发人员不用额外接入 C++ 的研究;通过对 Java 编译器的深入了解,有助于开发者分辨哪些代码是编译器可以帮我们处理的,哪些代码是需要我们自己调节以便编译器更好优化的;
上一篇:「JVM 编译优化」编译器优化技术
PS:感谢每一位志同道合者的阅读,欢迎关注、评论、赞!
参考资料:
- [1]《深入理解 Java 虚拟机》