「JVM 编译优化」Graal 编译器
创始人
2024-05-26 17:45:38
0

文章目录

      • 1. 历史背景
      • 2. 构建编译调试环境
      • 3. JVMCI 编译器接口
      • 4. 代码中间表示
      • 5. 代码优化与生成

1. 历史背景

Graal 编译器在 JDK 9 以 Jaotc 提前编译工具的形式首次加入到官方的 JDK 中,JDK 10 开始提供替换(得益于 HotSpot 编译器接口,Java Level JVM Compiler InterfaceJVMCI)服务端编译器支持;

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-NodesIdeal GraphStructured 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 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 虚拟机》

相关内容

热门资讯

【NI Multisim 14...   目录 序言 一、工具栏 🍊1.“标准”工具栏 🍊 2.视图工具...
银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
Android|无法访问或保存... 这个问题可能是由于权限设置不正确导致的。您需要在应用程序清单文件中添加以下代码来请求适当的权限:此外...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
AsusVivobook无法开... 首先,我们可以尝试重置BIOS(Basic Input/Output System)来解决这个问题。...
ASM贪吃蛇游戏-解决错误的问... 要解决ASM贪吃蛇游戏中的错误问题,你可以按照以下步骤进行:首先,确定错误的具体表现和问题所在。在贪...
月入8000+的steam搬砖... 大家好,我是阿阳 今天要给大家介绍的是 steam 游戏搬砖项目,目前...