Graal 编译器在 JDK 9 以 Jaotc
提前编译工具的形式首次加入到官方的 JDK 中,JDK 10 开始提供替换(得益于 HotSpot 编译器接口,Java Level JVM Compiler Interface
,JVMCI
)服务端编译器支持;
JVMCI 的三个功能
JVMCI 可以把一个 HotSpot VM 外部的、用 Java 语言实现的即时编译器(不限于 Graal)集成到 HotSpot 中,响应 HotSpot 发出的最顶层的编译请求,并将编译后的二进制代码部署到 HotSpot 的代码缓存;又绕开 HotSpot 的即时编译系统,让编译器直接为应用类库编译出二进制机器码(可当作提前编译器使用,如 Jaotc);
git clone https://github.com/graalvm/mx.git
export PATH=`pwd`/mx:$PATH
export JAVA_HOME=~/devtools/oraclejdk1.8.0_212-jvmci-20-b01
git clone https://github.com/graalvm/graal.git
cd graal/compiler
mx build
cd graal/compiler
mx eclipseinit
编译构建项目可能需要 2GB 已上内存,因此需要调大 IDE 的堆最大内存;
需将带有 JVMCI 的 JDK 作为 IDE 项目的编译运行使用的版本;
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() 方法;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;
}
相加操作
节点;除法操作
节点;公共子表达式消除演示
// 公共子表达式能够被消除示例
int workload(int a, int b) {return (a + b) * (a + b);
}// 公共子表达式是不可以被消除示例
int workload() {return (getA() + getB()) * (getA() + getB());
}
参数 0、1 的加法操作只进行了一次,却流出了两条数据给乘法操作;
四次方法调用全部执行,两个加法操作在两个独立节点进行,该版本不会进行公共子表达式消除;
Graal 编译器通过 greateGraph() 方法将字节码转成理想图;
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 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);}
}
算术聚合
、符号合并
等);全局规范化则在 CanonicalizerPhase::tryGlobalValueNumbering() 中实现;编译修改演示
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:感谢每一位志同道合者的阅读,欢迎关注、评论、赞!
参考资料: