Graal编译器介绍
Graal编译器由Java实现,支持提前编译和即时编译
JDK9推出Java虚拟机编译器接口(Java-Level JVM CompilerInterface,JVMCI),将Graal从HotSpot代码中独立出来(即可外部挂载)
构建编译调试环境
这里大坑,试了很多个版本都报错,文件放置在自己的Home下,不要放在奇奇怪怪的目录,保持同一个用户,否则可能会有权限问题!!!
下载构建工具mx
git clone https://github.com/graalvm/mx.git
git checkout 5.247.4
下载Graal编译器代码
git clone https://github.com/oracle/graal.git
git checkout release/graal-vm/19.3
使用带JVMCI的JDK 1.8,可在如下网站下载
https://github.com/graalvm/openjdk8-jvmci-builder/releases
Python版本如下
配置编译环境,将mx添加到环境变量,指定编译使用的JDK
export PATH=$PATH:/home/song/mx
export JAVA_HOME=/home/song/Downloads/jdk1.8
编译,无报错即编译成功
cd graal/compiler
mx build
编译完成后构建为Eclipse项目
cd graal/compiler
mx eclipseinit
下载Eclipse安装,修改eclipse.ini设置最大堆为2G
启动Eclipse,将编译graal的JDK设置为默认,Window-Java-installed JREs,删除原来的,点击add-Standard VM-Diectory选择存放目录会自动识别
在installed JREs下面的Execution Enviroments选择对应JDK并打勾
在Java-Compiler选择对应JDK
File-Import-General-Existing Projects into Workspace选择graal根目录导入项目
项目有报错但不影响下面步骤
JVMCI编译器接口
JVMCICompiler接口如下,除了接收字节码之外,还有方法的局部变量槽个数、操作数栈深度等信息
interface JVMCICompiler {
void compileMethod(CompilationRequest request);
}
interface CompilationRequest {
JavaMethod getMethod();
}
interface JavaMethod {
byte[] getCode();
int getMaxLocals();
int getMaxStackSize();
ProfilingInfo getProfilingInfo();
... // 省略其他方法
}
通过继承关系,可看到HotSpotGraalCompiler实现了JVMCI
对于如下程序
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;
}
}
先使用分层编译,添加虚拟机参数
-XX:+PrintCompilation -XX:CompileOnly=Demo::workload
可看到wordload()方法被分层编译了多次,“made not entrant”的输出就表示了方法的某个已编译版本被丢弃过
若只使用graal编译器运行,添加如下参数
-Djvmci.class.path.append=/home/song/graal/compiler/mxbuild/dists/jdk1.8/graal.jar:/home/song/graal/sdk/mxbuild/dists/jdk1.8/graal-sdk.jar
-XX:+UnlockExperimentalVMOptions
-XX:+EnableJVMCI
-XX:+UseJVMCICompiler
-XX:-TieredCompilation
-XX:+PrintCompilation
-XX:CompileOnly=Demo::workload
为了验证结果,修改HotSpotGraalCompiler类的compileMethod()方法,输出编译的方法名称和编译耗时
public CompilationRequestResult compileMethod(CompilationRequest request) {
long time = System.currentTimeMillis();
CompilationRequestResult result = compileMethod(request, true, graalRuntime.getOptions());
System.out.println("compile method:" + request.getMethod().getName());
System.out.println("time used:" + (System.currentTimeMillis() - time));
return result;
}
运行代码,可看到对应输出
代码中间表示
Graal采用理想图作为中间表示,其是有向图,节点表示程序的元素,边表示数据或控制流,对如下程序
public class Demo {
public static void main(String[] args) {
while (true) {
workload(14, 2);
}
}
static int workload(int a, int b) {
return (a + b) * (a + b);
}
}
添加参数-Dgraal.Dump输出理想图
-Djvmci.class.path.append=/home/song/graal/compiler/mxbuild/dists/jdk1.8/graal.jar:/home/song/graal/sdk/mxbuild/dists/jdk1.8/graal-sdk.jar
-XX:+UnlockExperimentalVMOptions
-XX:+EnableJVMCI
-XX:+UseJVMCICompiler
-XX:-TieredCompilation
-XX:+PrintCompilation
-XX:CompileOnly=Demo::workload
-Dgraal.Dump
可看到类似输出
在如下地址下载Ideal Graph Visualizer
https://www.oracle.com/technetwork/graalvm/downloads/index.html
修改idealgraphvisualizer/etc/idealgraphvisualizer.conf配置JDK
通过./idealgraphvisualizer/bin/idealgraphvisualizer打开软件,打开上面生成的文件
如上,参数的加法操作只进行了一次,然后流出两条数据流到乘法操作的输入中,说明产生了公共子表达式消除,而对于如下程序
public class Demo {
private static int A;
private static int B;
public static void main(String[] args) {
while (true) {
workload();
}
}
static int workload() {
return (getA() + getB()) * (getA() + getB());
}
public static int getA() {
return A;
}
public static int getB() {
return B;
}
}
getA()和getB()方法内部逻辑不确定的,只能在内联之后才能考虑进一步的优化措施,函数调用时无法实现公共子表达式消除,如下进行了4次调用方法、2次加法、1次乘法
代码优化
生成理想图的函数为greateGraph(),其数据结构为ValueNode子类节点集合,转换到理想图过程被封装在BytecodeParser类,
以BytecodeParser::genArithmeticOp()中的iadd操作为例,先出栈2个操作数,通过genIntegerAdd()相加再入栈
过程由AddNode节点表示,代码如下
protected ValueNode genIntegerAdd(ValueNode x, ValueNode y) {
return AddNode.create(x, y, NodeView.DEFAULT);
}
如下,AddNode在创建节点时调用canonical(),在其中优化缩减理想图(即代码优化),如进行了
- 算术聚合(如将(a+1)+2聚合为a+3)
- 符号合并(如将(a-b)+b合并为a)等操作
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);
}
}
而公众子表达式消除实现在tryGlobalValueNumbering()方法中,若发现了可以进行消除的算术子表达式,则找出重复的节点替换、删除,如下
public boolean tryGlobalValueNumbering(Node node, NodeClass<?> nodeClass) {
if (nodeClass.valueNumberable()) {
Node newNode = node.graph().findDuplicate(node);
if (newNode != null) {
assert !(node instanceof FixedNode || newNode instanceof FixedNode);
node.replaceAtUsagesAndDelete(newNode);
COUNTER_GLOBAL_VALUE_NUMBERING_HITS.increment(debug);
debug.log("GVN applied and new node is %1s", newNode);
return true;
}
}
return false;
}
机器码生成
先生成低级中间表示(LIR,与具体机器指令集相关的中间表示),再由HotSpot产生机器码,对如下程序
public class Demo {
public static void main(String[] args) {
while (true) {
workload(14, 2);
}
}
static int workload(int a, int b) {
return a+b;
}
}
添加虚拟机参数
-Djvmci.class.path.append=/home/song/graal/compiler/mxbuild/dists/jdk1.8/graal.jar:/home/song/graal/sdk/mxbuild/dists/jdk1.8/graal-sdk.jar
-XX:+UnlockExperimentalVMOptions
-XX:+EnableJVMCI
-XX:+UseJVMCICompiler
-XX:-TieredCompilation
-XX:+PrintAssembly
-XX:CompileOnly=Demo::workload
-XX:+DebugNonSafepoints
从下面地址下载hsdis-amd64.so放到$JAVA_HOME/lib/amd64
https://github.com/cmuramoto/hsdis
编译生成汇编如下
加法操作在AddNode::generate()中调用ArithmeticLIRGeneratorTool的emitAdd()方法生成机器码,将其改为emitSub
可看到对应汇编变成了sub