mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2026-05-11 04:04:34 +08:00
mod
This commit is contained in:
100
极客时间专栏/深入拆解Java虚拟机/模块四:黑科技/34 | Graal:用Java编译Java.md
Normal file
100
极客时间专栏/深入拆解Java虚拟机/模块四:黑科技/34 | Graal:用Java编译Java.md
Normal file
@@ -0,0 +1,100 @@
|
||||
<audio id="audio" title="34 | Graal:用Java编译Java" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/60/f3/6003fa06d5a4a1509ccddada9ec037f3.mp3"></audio>
|
||||
|
||||
最后这三篇文章,我将介绍Oracle Labs的GraalVM项目。
|
||||
|
||||
GraalVM是一个高性能的、支持多种编程语言的执行环境。它既可以在传统的OpenJDK上运行,也可以通过AOT(Ahead-Of-Time)编译成可执行文件单独运行,甚至可以集成至数据库中运行。
|
||||
|
||||
除此之外,它还移除了编程语言之间的边界,并且支持通过即时编译技术,将混杂了不同的编程语言的代码编译到同一段二进制码之中,从而实现不同语言之间的无缝切换。
|
||||
|
||||
今天这一篇,我们就来讲讲GraalVM的基石Graal编译器。
|
||||
|
||||
在之前的篇章中,特别是介绍即时编译技术的第二部分,我们反反复复提到了Graal编译器。这是一个用Java写就的即时编译器,它从Java 9u开始便被集成自JDK中,作为实验性质的即时编译器。
|
||||
|
||||
Graal编译器可以通过Java虚拟机参数`-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler`启用。当启用时,它将替换掉HotSpot中的C2编译器,并响应原本由C2负责的编译请求。
|
||||
|
||||
在今天的文章中,我将详细跟你介绍一下Graal与Java虚拟机的交互、Graal和C2的区别以及Graal的实现细节。
|
||||
|
||||
## Graal和Java虚拟机的交互
|
||||
|
||||
我们知道,即时编译器是Java虚拟机中相对独立的模块,它主要负责接收Java字节码,并生成可以直接运行的二进制码。
|
||||
|
||||
具体来说,即时编译器与Java虚拟机的交互可以分为如下三个方面。
|
||||
|
||||
1. 响应编译请求;
|
||||
1. 获取编译所需的元数据(如类、方法、字段)和反映程序执行状态的profile;
|
||||
1. 将生成的二进制码部署至代码缓存(code cache)里。
|
||||
|
||||
即时编译器通过这三个功能组成了一个响应编译请求、获取编译所需的数据,完成编译并部署的完整编译周期。
|
||||
|
||||
传统情况下,即时编译器是与Java虚拟机紧耦合的。也就是说,对即时编译器的更改需要重新编译整个Java虚拟机。这对于开发相对活跃的Graal来说显然是不可接受的。
|
||||
|
||||
为了让Java虚拟机与Graal解耦合,我们引入了[Java虚拟机编译器接口](http://openjdk.java.net/jeps/243)(JVM Compiler Interface,JVMCI),将上述三个功能抽象成一个Java层面的接口。这样一来,在Graal所依赖的JVMCI版本不变的情况下,我们仅需要替换Graal编译器相关的jar包(Java 9以后的jmod文件),便可完成对Graal的升级。
|
||||
|
||||
JVMCI的作用并不局限于完成由Java虚拟机发出的编译请求。实际上,Java程序可以直接调用Graal,编译并部署指定方法。
|
||||
|
||||
Graal的单元测试便是基于这项技术。为了测试某项优化是否起作用,原本我们需要反复运行某一测试方法,直至Graal收到由Java虚拟机发出针对该方法的编译请求,而现在我们可以直接指定编译该方法,并进行测试。我们下一篇将介绍的Truffle语言实现框架,同样也是基于这项技术的。
|
||||
|
||||
## Graal和C2的区别
|
||||
|
||||
Graal和C2最为明显的一个区别是:Graal是用Java写的,而C2是用C++写的。相对来说,Graal更加模块化,也更容易开发与维护,毕竟,连C2的作者Cliff Click大神都不想重蹈用C++开发Java虚拟机的覆辙。
|
||||
|
||||
许多开发者会觉得用C++写的C2肯定要比Graal快。实际上,在充分预热的情况下,Java程序中的热点代码早已经通过即时编译转换为二进制码,在执行速度上并不亚于静态编译的C++程序。
|
||||
|
||||
再者,即便是解释执行Graal,也仅是会减慢编译效率,而并不影响编译结果的性能。
|
||||
|
||||
换句话说,如果C2和Graal采用相同的优化手段,那么它们的编译结果是一样的。所以,程序达到稳定状态(即不再触发新的即时编译)的性能,也就是峰值性能,将也是一样的。
|
||||
|
||||
由于Java语言容易开发维护的优势,我们可以很方便地将C2的新优化移植到Graal中。反之则不然,比如,在Graal中被证实有效的部分逃逸分析(partial escape analysis)至今未被移植到C2中。
|
||||
|
||||
Graal和C2另一个优化上的分歧则是方法内联算法。相对来说,Graal的内联算法对新语法、新语言更加友好,例如Java 8的lambda表达式以及Scala语言。
|
||||
|
||||
我们曾统计过数十个Java或Scala程序的峰值性能。总体而言,Graal编译结果的性能要优于C2。对于Java程序来说,Graal的优势并不明显;对于Scala程序来说,Graal的性能优势达到了10%。
|
||||
|
||||
大规模使用Scala的Twitter便在他们的生产环境中部署了Graal编译器,并取得了11%的性能提升。([Slides](https://downloads.ctfassets.net/oxjq45e8ilak/6eh2A72b4IyWsWOIcig4K0/cbb664566fe86672d92ddfb210623920/Chris_Thalinger_Twitter_s_quest_for_a_wholly_Graal_runtime.pdf), [Video](https://youtu.be/G-vlQaPMAxg?t=20m15s),该数据基于GraalVM社区版。)
|
||||
|
||||
## Graal的实现
|
||||
|
||||
Graal编译器将编译过程分为前端和后端两大部分。前端用于实现平台无关的优化(如方法内联),以及小部分平台相关的优化;而后端则负责大部分的平台相关优化(如寄存器分配),以及机器码的生成。
|
||||
|
||||
在介绍即时编译技术时,我曾提到过,Graal和C2都采用了Sea-of-Nodes IR。严格来说,这里指的是Graal的前端,而后端采用的是另一种非Sea-of-Nodes的IR。通常,我们将前端的IR称之为High-level IR,或者HIR;后端的IR则称之为Low-level IR,或者LIR。
|
||||
|
||||
Graal的前端是由一个个单独的优化阶段(optimization phase)构成的。我们可以将每个优化阶段想象成一个图算法:它会接收一个规则的图,遍历图上的节点并做出优化,并且返回另一个规则的图。前端中的编译阶段除了少数几个关键的之外,其余均可以通过配置选项来开启或关闭。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d9/b8/d9772c569c25eabb7c2e7af53878e3b8.png" alt="" />
|
||||
|
||||
Graal编译器前端的优化阶段(局部)
|
||||
|
||||
>
|
||||
<p>感兴趣的同学可以阅读Graal repo里配置这些编译优化阶段的源文件<br />
|
||||
[HighTier.java](https://github.com/oracle/graal/blob/master/compiler/src/org.graalvm.compiler.core/src/org/graalvm/compiler/core/phases/HighTier.java),[MidTier.java](https://github.com/oracle/graal/blob/master/compiler/src/org.graalvm.compiler.core/src/org/graalvm/compiler/core/phases/MidTier.java),以及[LowTier.java](https://github.com/oracle/graal/blob/master/compiler/src/org.graalvm.compiler.core/src/org/graalvm/compiler/core/phases/LowTier.java)。</p>
|
||||
|
||||
|
||||
我们知道,Graal和C2都采用了激进的投机性优化手段(speculative optimization)。
|
||||
|
||||
通常,这些优化都基于某种假设(assumption)。当假设出错的情况下,Java虚拟机会借助去优化(deoptimization)这项机制,从执行即时编译器生成的机器码切换回解释执行,在必要情况下,它甚至会废弃这份机器码,并在重新收集程序profile之后,再进行编译。
|
||||
|
||||
举个以前讲过的例子,类层次分析。在进行虚方法内联时(或者其他与类层次相关的优化),我们可能会发现某个接口仅有一个实现。
|
||||
|
||||
在即时编译过程中,我们可以假设在之后的执行过程中仍旧只有这一个实现,并根据这个假设进行编译优化。当之后加载了接口的另一实现时,我们便会废弃这份机器码。
|
||||
|
||||
Graal与C2相比会更加激进。它从设计上便十分青睐这种基于假设的优化手段。在编译过程中,Graal支持自定义假设,并且直接与去优化节点相关联。
|
||||
|
||||
当对应的去优化被触发时,Java虚拟机将负责记录对应的自定义假设。而Graal在第二次编译同一方法时,便会知道该自定义假设有误,从而不再对该方法使用相同的激进优化。
|
||||
|
||||
Java虚拟机的另一个能够大幅度提升性能的特性是intrinsic方法,我在之前的篇章中已经详细介绍过了。在Graal中,实现高性能的intrinsic方法也相对比较简单。Graal提供了一种替换方法调用的机制,在解析Java字节码时会将匹配到的方法调用,替换成对另一个内部方法的调用,或者直接替换为特殊节点。
|
||||
|
||||
举例来说,我们可以把比较两个byte数组的方法`java.util.Arrays.equals(byte[],byte[])`替换成一个特殊节点,用来代表整个数组比较的逻辑。这样一来,当前编译方法所对应的图将被简化,因而其适用于其他优化的可能性也将提升。
|
||||
|
||||
## 总结与实践
|
||||
|
||||
Graal是一个用Java写就的、并能够将Java字节码转换成二进制码的即时编译器。它通过JVMCI与Java虚拟机交互,响应由后者发出的编译请求、完成编译并部署编译结果。
|
||||
|
||||
对Java程序而言,Graal编译结果的性能略优于OpenJDK中的C2;对Scala程序而言,它的性能优势可达到10%(企业版甚至可以达到20%!)。这背后离不开Graal所采用的激进优化方式。
|
||||
|
||||
今天的实践环节,你可以尝试使用附带Graal编译器的JDK。在Java 10,11中,你可以通过添加虚拟机参数`-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler`来启用,或者下载我们部署在[Oracle OTN](https://www.oracle.com/technetwork/oracle-labs/program-languages/downloads/index.html)上的基于Java 8的版本。
|
||||
|
||||
>
|
||||
在刚开始运行的过程中,Graal编译器本身需要被即时编译,会抢占原本可用于编译应用代码的计算资源。因此,目前Graal编译器的启动性能会较差。最后一篇我会介绍解决方案。
|
||||
|
||||
|
||||
|
||||
207
极客时间专栏/深入拆解Java虚拟机/模块四:黑科技/35 | Truffle:语言实现框架.md
Normal file
207
极客时间专栏/深入拆解Java虚拟机/模块四:黑科技/35 | Truffle:语言实现框架.md
Normal file
@@ -0,0 +1,207 @@
|
||||
<audio id="audio" title="35 | Truffle:语言实现框架" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/2b/05/2bf0b4760e1aac3dc495918fe4dd0a05.mp3"></audio>
|
||||
|
||||
今天我们来聊聊GraalVM中的语言实现框架Truffle。
|
||||
|
||||
我们知道,实现一门新编程语言的传统做法是实现一个编译器,也就是把用该语言编写的程序转换成可直接在硬件上运行的机器码。
|
||||
|
||||
通常来说,编译器分为前端和后端:前端负责词法分析、语法分析、类型检查和中间代码生成,后端负责编译优化和目标代码生成。
|
||||
|
||||
不过,许多编译器教程只涉及了前端中的词法分析和语法分析,并没有真正生成可以运行的目标代码,更谈不上编译优化,因此在生产环境中并不实用。
|
||||
|
||||
另一种比较取巧的做法则是将新语言编译成某种已知语言,或者已知的中间形式,例如将Scala、Kotlin编译成Java字节码。
|
||||
|
||||
这样做的好处是可以直接享用Java虚拟机自带的各项优化,包括即时编译、自动内存管理等等。因此,这种做法对所生成的Java字节码的优化程度要求不高。
|
||||
|
||||
不过,不管是附带编译优化的编译器,还是生成中间形式并依赖于其他运行时的即时编译优化的编译器,它们所针对的都是[编译型语言](https://en.wikipedia.org/wiki/Compiled_language),在运行之前都需要这一额外的编译步骤。
|
||||
|
||||
与编译型语言相对应的则是[解释型语言](https://en.wikipedia.org/wiki/Interpreted_language),例如JavaScript、Ruby、Python等。对于这些语言来说,它们无须额外的编译步骤,而是依赖于解释执行器进行解析并执行。
|
||||
|
||||
为了让该解释执行器能够高效地运行大型程序,语言实现开发人员通常会将其包装在虚拟机里,并实现诸如即时编译、垃圾回收等其他组件。这些组件对语言设计 本身并无太大贡献,仅仅是为了实用性而不得不进行的工程实现。
|
||||
|
||||
在理想情况下,我们希望在不同的语言实现中复用这些组件。也就是说,每当开发一门新语言时,我们只需要实现它的解释执行器,便能够直接复用即时编译、垃圾回收等组件,从而达到高性能的效果。这也是Truffle项目的目标。接下来,我们就来讲讲这个项目。
|
||||
|
||||
## Truffle项目简介
|
||||
|
||||
Truffle是一个用Java写就的语言实现框架。基于Truffle的语言实现仅需用Java实现词法分析、语法分析以及针对语法分析所生成的抽象语法树(Abstract Syntax Tree,AST)的解释执行器,便可以享用由Truffle提供的各项运行时优化。
|
||||
|
||||
就一个完整的Truffle语言实现而言,由于实现本身以及其所依赖的Truffle框架部分都是用Java实现的,因此它可以运行在任何Java虚拟机之上。
|
||||
|
||||
当然,如果Truffle运行在附带了Graal编译器的Java虚拟机之上,那么它将调用Graal编译器所提供的API,主动触发对Truffle语言的即时编译,将对AST的解释执行转换为执行即时编译后的机器码。
|
||||
|
||||
在这种情况下,Graal编译器相当于一个提供了即时编译功能的库,宿主虚拟机本身仍可使用C2作为其唯一的即时编译器,或者分层编译模式下的4层编译器。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/20/30/20c7a514f226689536fafc6886a08e30.png" alt="" />
|
||||
|
||||
我们团队实现并且开源了多个Truffle语言,例如[JavaScript](https://github.com/graalvm/graaljs),[Ruby](https://github.com/oracle/truffleruby),[R](https://github.com/oracle/fastr),[Python](https://github.com/graalvm/graalpython),以及可用来解释执行LLVM bitcode的[Sulong](https://github.com/oracle/graal/tree/master/sulong)。关于Sulong项目,任何能够编译为LLVM bitcode的编程语言,例如C/C++,都能够在这上面运行。
|
||||
|
||||
下图展示了运行在GraalVM EE上的Java虚拟机语言,以及除Python外Truffle语言的峰值性能指标(2017年数据)。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0a/44/0aa87b77b2d6eb0147d4a2b342b0d644.png" alt="" />
|
||||
|
||||
这里我采用的基线是每个语言较有竞争力的语言实现。
|
||||
|
||||
- 对于Java虚拟机语言(Java、Scala),我比较的是使用C2的HotSpot虚拟机和使用Graal的HotSpot虚拟机。
|
||||
- 对于Ruby,我比较的是运行在HotSpot虚拟机之上的JRuby和Truffle Ruby。
|
||||
- 对于R,我比较的是GNU R和基于Truffle的FastR。
|
||||
- 对于C/C++,我比较的是利用LLVM编译器生成的二进制文件和基于Truffle的Sulong。
|
||||
- 对于JavaScript,我比较的是Google的V8和Graal.js。
|
||||
|
||||
针对每种语言,我们运行了上百个基准测试,求出各个基准测试**峰值性能**的加速比,并且汇总成图中所示的几何平均值(Geo. mean)。
|
||||
|
||||
简单地说明一下,当GraalVM的加速比为1时,代表使用其他语言实现和使用GraalVM的性能相当。当GraalVM加速比超过1时,则代表GraalVM的性能较好;反之,则说明GraalVM的性能较差。
|
||||
|
||||
我们可以看到,Java跑在Graal上和跑在C2上的执行效率类似,而Scala跑在Graal上的执行效率则是跑在C2上的1.2倍。
|
||||
|
||||
对于Ruby或者R这类解释型语言,经由Graal编译器加速的Truffle语言解释器的性能十分优越,分别达到对应基线的4.1x和4.5x。这里便可以看出使用专业即时编译器的Truffle框架的优势所在。
|
||||
|
||||
不过,对于同样拥有专业即时编译器的V8来说,基于Truffle的Graal.js仍处于追赶者的位置。考虑到我们团队中负责Graal.js的工程师仅有个位数,能够达到如此性能已属不易。现在Graal.js已经开源出来,我相信借助社区的贡献,它的性能能够得到进一步的提升。
|
||||
|
||||
Sulong与传统的C/C++相比,由于两者最终都将编译为机器码,因此原则上后者定义了前者的性能上限。
|
||||
|
||||
不过,Sulong将C/C++代码放在托管环境中运行,所有代码中的内存访问都会在托管环境的监控之下。无论是会触发Segfault的异常访问,还是读取敏感数据的恶意访问,都能够被Sulong拦截下来并作出相应处理。
|
||||
|
||||
## Partial Evaluation
|
||||
|
||||
如果要理解Truffle的原理,我们需要先了解Partial Evaluation这一个概念。
|
||||
|
||||
假设有一段程序`P`,它将一系列输入`I`转换成输出`O`(即`P: I -> O`)。而这些输入又可以进一步划分为编译时已知的常量`IS`,和编译时未知的`ID`。
|
||||
|
||||
那么,我们可以将程序`P: I -> O`转换为等价的另一段程序`P': ID -> O`。这个新程序`P'`便是`P`的特化(Specialization),而从`P`转换到`P'`的这个过程便是所谓的Partial Evaluation。
|
||||
|
||||
回到Truffle这边,我们可以将Truffle语言的解释执行器当成`P`,将某段用Truffle语言写就的程序当作`IS`,并通过Partial Evaluation特化为`P'`。由于Truffle语言的解释执行器是用Java写的,因此我们可以利用Graal编译器将`P'`编译为二进制码。
|
||||
|
||||
下面我将用一个具体例子来讲解。
|
||||
|
||||
假设有一门语言X,只支持读取整数参数和整数加法。这两种操作分别对应下面这段代码中的AST节点`Arg`和`Add`。
|
||||
|
||||
```
|
||||
abstract class Node {
|
||||
abstract int execute(int[] args);
|
||||
}
|
||||
|
||||
class Arg extends Node {
|
||||
final int index;
|
||||
|
||||
Arg(int i) { this.index = i; }
|
||||
|
||||
int execute(int[] args) {
|
||||
return args[index];
|
||||
}
|
||||
}
|
||||
|
||||
class Add extends Node {
|
||||
final Node left, right;
|
||||
|
||||
Add(Node left, Node right) {
|
||||
this.left = left;
|
||||
this.right = right;
|
||||
}
|
||||
|
||||
int execute(int[] args) {
|
||||
return left.execute(args) +
|
||||
right.execute(args);
|
||||
}
|
||||
}
|
||||
|
||||
static int interpret(Node node, int[] args) {
|
||||
return node.execute(args);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
所谓AST节点的解释执行,便是调用这些AST节点的`execute`方法;而一段程序的解释执行,则是调用这段程序的AST根节点的`execute`方法。
|
||||
|
||||
我们可以看到,`Arg`节点和`Add`节点均实现了`execute`方法,接收一个用来指代程序输入的int数组参数,并返回计算结果。其中,`Arg`节点将返回int数组的第`i`个参数(`i`是硬编码在程序之中的常量);而`Add`节点将分别调用左右两个节点的`execute`方法, 并将所返回的值相加后再返回。
|
||||
|
||||
下面我们将利用语言X实现一段程序,计算三个输入参数之和`arg0 + arg1 + arg2`。这段程序解析生成的AST如下述代码所示:
|
||||
|
||||
```
|
||||
// Sample program: arg0 + arg1 + arg2
|
||||
sample = new Add(new Add(new Arg(0), new Arg(1)), new Arg(2));
|
||||
|
||||
```
|
||||
|
||||
这段程序对应的解释执行则是`interpret(sample, args)`,其中`args`为代表传入参数的int数组。由于`sample`是编译时常量,因此我们可以将其通过Partial Evaluation,特化为下面这段代码所示的`interpret0`方法:
|
||||
|
||||
```
|
||||
static final Node sample = new Add(new Add(new Arg(0), new Arg(1)), new Arg(2));
|
||||
|
||||
static int interpret0(int[] args) {
|
||||
return sample.execute(args);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Truffle的Partial Evaluator会不断进行方法内联(直至遇到被``@TruffleBoundary`注解的方法)。因此,上面这段代码的`interpret0`方法,在内联了对`Add.execute`方法的调用之后,会转换成下述代码:
|
||||
|
||||
```
|
||||
static final Node sample = new Add(new Add(new Arg(0), new Arg(1)), new Arg(2));
|
||||
|
||||
static int interpret0(int[] args) {
|
||||
return sample.left.execute(args) + sample.right.execute(args);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
同样,我们可以进一步内联对`Add.execute`方法的调用以及对`Arg.execute`方法的调用,最终将`interpret0`转换成下述代码:
|
||||
|
||||
```
|
||||
static int interpret0(int[] args) {
|
||||
return args[0] + args[1] + args[2];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
至此,我们已成功地将一段Truffle语言代码的解释执行转换为上述Java代码。接下来,我们便可以让Graal编译器将这段Java代码编译为机器码,从而实现Truffle语言的即时编译。
|
||||
|
||||
## 节点重写
|
||||
|
||||
Truffle的另一项关键优化是节点重写(node rewriting)。
|
||||
|
||||
在动态语言中,许多变量的类型是在运行过程中方能确定的。以加法符号`+`为例,它既可以表示整数加法,还可以表示浮点数加法,甚至可以表示字符串加法。
|
||||
|
||||
如果是静态语言,我们可以通过推断加法的两个操作数的具体类型,来确定该加法的类型。但对于动态语言来说,我们需要在运行时动态确定操作数的具体类型,并据此选择对应的加法操作。这种在运行时选择语义的节点,会十分不利于即时编译,从而严重影响到程序的性能。
|
||||
|
||||
Truffle语言解释器会收集每个AST节点所代表的操作的类型,并且在即时编译时,作出针对所收集得到的类型profile的特化(specialization)。
|
||||
|
||||
还是以加法操作为例,如果所收集的类型profile显示这是一个整数加法操作,那么在即时编译时我们会将对应的AST节点当成整数加法;如果是一个字符串加法操作,那么我们会将对应的AST节点当成字符串加法。
|
||||
|
||||
当然,如果该加法操作既有可能是整数加法也可能是字符串加法,那么我们只好在运行过程中判断具体的操作类型,并选择相应的加法操作。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/54/8f/543ee374164fd43f2773043c675b568f.png" alt="" />
|
||||
|
||||
这种基于类型profile的优化,与我们以前介绍过的Java虚拟机中解释执行器以及三层C1编译代码十分类似,它们背后的核心都是基于假设的投机性优化,以及在假设失败时的去优化。
|
||||
|
||||
在即时编译过后,如果运行过程中发现AST节点的实际类型和所假设的类型不同,Truffle会主动调用Graal编译器提供的去优化API,返回至解释执行AST节点的状态,并且重新收集AST节点的类型信息。之后,Truffle会再次利用Graal编译器进行新一轮的即时编译。
|
||||
|
||||
当然,如果能够在第一次编译时便已达到稳定状态,不再触发去优化以及重新编译,那么,这会极大地减短程序到达峰值性能的时间。为此,我们统计了各个Truffle语言的方法在进行过多少次方法调用后,其AST节点的类型会固定下来。
|
||||
|
||||
据统计,在JavaScript方法和Ruby方法中,80%会在5次方法调用后稳定下来,90%会在7次调用后稳定下来,99%会在19次方法调用之后稳定下来。
|
||||
|
||||
R语言的方法则比较特殊,即便是不进行任何调用,有50%的方法已经稳定下来了。这背后的原因也不难推测,这是因为R语言主要用于数值统计,几乎所有的操作都是浮点数类型的。
|
||||
|
||||
## Polyglot
|
||||
|
||||
在开发过程中,我们通常会为工程项目选定一门语言,但问题也会接踵而至:一是这门语言没有实现我们可能需要用到的库,二是这门语言并不适用于某类问题。
|
||||
|
||||
Truffle语言实现框架则支持Polyglot,允许在同一段代码中混用不同的编程语言,从而使得开发人员能够自由地选择合适的语言来实现子组件。
|
||||
|
||||
与其他Polyglot框架不同的是,Truffle语言之间能够共用对象。也就是说,在不对某个语言中的对象进行复制或者序列化反序列化的情况下,Truffle可以无缝地将该对象传递给另一门语言。因此,Truffle的Polyglot在切换语言时,性能开销非常小,甚至经常能够达到零开销。
|
||||
|
||||
Truffle的Polyglot特性是通过Polyglot API来实现的。每个实现了Polyglot API的Truffle语言,其对象都能够被其他Truffle语言通过Polyglot API解析。实际上,当通过Polyglot API解析外来对象时,我们并不需要了解对方语言,便能够识别其数据结构,访问其中的数据,并进行进一步的计算。
|
||||
|
||||
## 总结与实践
|
||||
|
||||
今天我介绍了GraalVM中的Truffle项目。
|
||||
|
||||
Truffle是一个语言实现框架,允许语言开发者在仅实现词法解析、语法解析以及AST解释器的情况下,达到极佳的性能。目前Oracle Labs已经实现并维护了JavaScript、Ruby、R、Python以及可用于解析LLVM bitcode的Sulong。后者将支持在GraalVM上运行C/C++代码。
|
||||
|
||||
Truffle背后所依赖的技术是Partial Evaluation以及节点重写。Partial Evaluation指的是将所要编译的目标程序解析生成的抽象语法树当做编译时常量,特化该Truffle语言的解释器,从而得到指代这段程序解释执行过程的Java代码。然后,我们可以借助Graal编译器将这段Java代码即时编译为机器码。
|
||||
|
||||
节点重写则是收集AST节点的类型,根据所收集的类型profile进行的特化,并在节点类型不匹配时进行去优化并重新收集、编译的一项技术。
|
||||
|
||||
Truffle的Polyglot特性支持在一段代码中混用多种不同的语言。与其他Polyglot框架相比,它支持在不同的Truffle语言中复用内存中存储的同一个对象。
|
||||
|
||||
今天的实践环节,请你试用GraalVM中附带的各项语言实现。你可以运行我们官网上的各个[示例程序](https://www.graalvm.org/docs/examples/)。
|
||||
|
||||
|
||||
106
极客时间专栏/深入拆解Java虚拟机/模块四:黑科技/36 | SubstrateVM:AOT编译框架.md
Normal file
106
极客时间专栏/深入拆解Java虚拟机/模块四:黑科技/36 | SubstrateVM:AOT编译框架.md
Normal file
@@ -0,0 +1,106 @@
|
||||
<audio id="audio" title="36 | SubstrateVM:AOT编译框架" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/60/b4/600d2b337171381b5f16347bc8abc5b4.mp3"></audio>
|
||||
|
||||
今天我们来聊聊GraalVM中的Ahead-Of-Time(AOT)编译框架SubstrateVM。
|
||||
|
||||
先来介绍一下AOT编译,所谓AOT编译,是与即时编译相对立的一个概念。我们知道,即时编译指的是在程序的运行过程中,将字节码转换为可在硬件上直接运行的机器码,并部署至托管环境中的过程。
|
||||
|
||||
而AOT编译指的则是,在**程序运行之前**,便将字节码转换为机器码的过程。它的成果可以是需要链接至托管环境中的动态共享库,也可以是独立运行的可执行文件。
|
||||
|
||||
狭义的AOT编译针对的目标代码需要与即时编译的一致,也就是针对那些原本可以被即时编译的代码。不过,我们也可以简单地将AOT编译理解为类似于GCC的静态编译器。
|
||||
|
||||
AOT编译的优点显而易见:我们无须在运行过程中耗费CPU资源来进行即时编译,而程序也能够在启动伊始就达到理想的性能。
|
||||
|
||||
然而,与即时编译相比,AOT编译无法得知程序运行时的信息,因此也无法进行基于类层次分析的完全虚方法内联,或者基于程序profile的投机性优化(并非硬性限制,我们可以通过限制运行范围,或者利用上一次运行的程序profile来绕开这两个限制)。这两者都会影响程序的峰值性能。
|
||||
|
||||
Java 9引入了实验性AOT编译工具[jaotc](http://openjdk.java.net/jeps/295)。它借助了Graal编译器,将所输入的Java类文件转换为机器码,并存放至生成的动态共享库之中。
|
||||
|
||||
在启动过程中,Java虚拟机将加载参数`-XX:AOTLibrary`所指定的动态共享库,并部署其中的机器码。这些机器码的作用机理和即时编译生成的机器码作用机理一样,都是在方法调用时切入,并能够去优化至解释执行。
|
||||
|
||||
由于Java虚拟机可能通过Java agent或者C agent改动所加载的字节码,或者这份AOT编译生成的机器码针对的是旧版本的Java类,因此它需要额外的验证机制,来保证即将链接的机器码的语义与对应的Java类的语义是一致的。
|
||||
|
||||
jaotc使用的机制便是类指纹(class fingerprinting)。它会在动态共享库中保存被AOT编译的Java类的摘要信息。在运行过程中,Java虚拟机负责将该摘要信息与已加载的Java类相比较,一旦不匹配,则直接舍弃这份AOT编译的机器码。
|
||||
|
||||
jaotc的一大应用便是编译java.base module,也就是Java核心类库中最为基础的类。这些类很有可能会被应用程序所调用,但调用频率未必高到能够触发即时编译。
|
||||
|
||||
因此,如果Java虚拟机能够使用AOT编译技术,将它们提前编译为机器码,那么将避免在执行即时编译生成的机器码时,因为“不小心”调用到这些基础类,而需要切换至解释执行的性能惩罚。
|
||||
|
||||
不过,今天要介绍的主角并非jaotc,而是同样使用了Graal编译器的AOT编译框架SubstrateVM。
|
||||
|
||||
## SubstrateVM的设计与实现
|
||||
|
||||
SubstrateVM的设计初衷是提供一个高启动性能、低内存开销,并且能够无缝衔接C代码的Java运行时。它与jaotc的区别主要有两处。
|
||||
|
||||
第一,SubstrateVM脱离了HotSpot虚拟机,并拥有独立的运行时,包含异常处理,同步,线程管理,内存管理(垃圾回收)和JNI等组件。
|
||||
|
||||
第二,SubstrateVM要求目标程序是封闭的,即不能动态加载其他类库等。基于这个假设,SubstrateVM将探索整个编译空间,并通过静态分析推算出所有虚方法调用的目标方法。最终,SubstrateVM会将所有可能执行到的方法都纳入编译范围之中,从而免于实现额外的解释执行器。
|
||||
|
||||
>
|
||||
有关SubstrateVM的其他限制,你可以参考[这篇文档](https://github.com/oracle/graal/blob/master/substratevm/LIMITATIONS.md)。
|
||||
|
||||
|
||||
从执行时间上来划分,SubstrateVM可分为两部分:native image generator以及SubstrateVM运行时。后者SubstrateVM运行时便是前面提到的精简运行时,经过AOT编译的目标程序将跑在该运行时之上。
|
||||
|
||||
native image generator则包含了真正的AOT编译逻辑。它本身是一个Java程序,将使用Graal编译器将Java类文件编译为可执行文件或者动态链接库。
|
||||
|
||||
在进行编译之前,native image generator将采用指针分析(points-to analysis),从用户提供的程序入口出发,探索所有可达的代码。在探索的同时,它还将执行初始化代码,并在最终生成可执行文件时,将已初始化的堆保存至一个堆快照之中。这样一来,SubstrateVM将直接从目标程序开始运行,而无须重复进行Java虚拟机的初始化。
|
||||
|
||||
SubstrateVM主要用于Java虚拟机语言的AOT编译,例如Java、Scala以及Kotlin。Truffle语言实现本质上就是Java程序,而且它所有用到的类都是编译时已知的,因此也适合在SubstrateVM上运行。不过,它并不会AOT编译用Truffle语言写就的程序。
|
||||
|
||||
## SubstrateVM的启动时间与内存开销
|
||||
|
||||
SubstrateVM的启动时间和内存开销非常少。我们曾比较过用C和用Java两种语言写就的Hello World程序。C程序的执行时间在10ms以下,内存开销在500KB以下。在HotSpot虚拟机上运行的Java程序则需要40ms,内存开销为24MB。
|
||||
|
||||
使用SubstrateVM的Java程序的执行时间则与C程序持平,内存开销在850KB左右。这得益于SubstrateVM所保存的堆快照,以及无须额外初始化,直接执行目标代码的特性。
|
||||
|
||||
同样,我们还比较了用JavaScript编写的Hello World程序。这里的测试对象是Google的V8以及基于Truffle的Graal.js。这两个执行引擎都涉及了大量的解析代码以及执行代码,因此可以当作大型应用程序来看待。
|
||||
|
||||
V8的执行效率非常高,能够与C程序的Hello World相媲美,但是它使用了约18MB的内存。运行在HotSpot虚拟机上的Graal.js则需要650ms方能执行完这段JavaScript的Hello World程序,而且内存开销在120MB左右。
|
||||
|
||||
运行在SubstrateVM上的Graal.js无论是执行时间还是内存开销都十分优越,分别为10ms以下以及4.2MB。我们可以看到,它在运行时间与V8持平的情况下,内存开销远小于V8。
|
||||
|
||||
由于SubstrateVM的轻量特性,它十分适合于嵌入至其他系统之中。Oracle Labs的另一个团队便是将Truffle语言实现嵌入至Oracle数据库之中,这样就可以在数据库中运行任意语言的预储程序(stored procedure)。如果你感兴趣的话,可以搜索Oracle Database Multilingual Engine(MLE),或者参阅这个[网址](https://www.oracle.com/technetwork/database/multilingual-engine/overview/index.html)。我们团队也在与MySQL合作,开发MySQL MLE,详情可留意我们在今年Oracle Code One的[讲座](https://oracle.rainfocus.com/widget/oracle/oow18/catalogcodeone18?search=MySQL%20JavaScript)。
|
||||
|
||||
## Metropolis项目
|
||||
|
||||
去年OpenJDK推出了[Metropolis项目](http://openjdk.java.net/projects/metropolis/),他们希望可以实现“Java-on-Java”的远大目标。
|
||||
|
||||
我们知道,目前HotSpot虚拟机的绝大部分代码都是用C++写的。这也造就了一个非常有趣的现象,那便是对Java语言本身的贡献需要精通C++。此外,随着HotSpot项目日渐庞大,维护难度也逐渐上升。
|
||||
|
||||
由于上述种种原因,使用Java来开发Java虚拟机的呼声越来越高。Oracle的架构师John Rose便提出了使用Java开发Java虚拟机的四大好处:
|
||||
|
||||
1. 能够完全控制编译Java虚拟机时所使用的优化技术;
|
||||
1. 能够与C++语言的更新解耦合;
|
||||
1. 能够减轻开发人员以及维护人员的负担;
|
||||
1. 能够以更为敏捷的方式实现Java的新功能。
|
||||
|
||||
当然,Metropolis项目并非第一个提出Java-on-Java概念的项目。实际上,[JikesRVM项目](https://www.jikesrvm.org/)和[Maxine VM项目](https://github.com/beehive-lab/Maxine-VM)都已用Java完整地实现了一套Java虚拟机(后者的即时编译器C1X便是Graal编译器的前身)。
|
||||
|
||||
然而,Java-on-Java技术通常会干扰应用程序的垃圾回收、即时编译优化,从而严重影响Java虚拟机的启动性能。
|
||||
|
||||
举例来说,目前使用了Graal编译器的HotSpot虚拟机会在即时编译过程中生成大量的Java对象,这些Java对象同样会占据应用程序的堆空间,从而使得垃圾回收更加频繁。
|
||||
|
||||
另外,Graal编译器本身也会触发即时编译,并与应用程序的即时编译竞争编译线程的CPU资源。这将造成应用程序从解释执行切换至即时编译生成的机器码的时间大大地增长,从而降低应用程序的启动性能。
|
||||
|
||||
Metropolis项目的第一个子项目便是探索部署已AOT编译的Graal编译器的可能性。这个子项目将借助SubstrateVM技术,把整个Graal编译器AOT编译为机器码。
|
||||
|
||||
这样一来,在运行过程中,Graal编译器不再需要被即时编译,因此也不会再占据可用于即时编译应用程序的CPU资源,使用Graal编译器的HotSpot虚拟机的启动性能将得到大幅度地提升。
|
||||
|
||||
此外,由于SubstrateVM编译得到的Graal编译器将使用独立的堆空间,因此Graal编译器在即时编译过程中生成的Java对象将不再干扰应用程序所使用的堆空间。
|
||||
|
||||
目前Metropolis项目仍处于前期验证阶段,如果你感兴趣的话,可以关注之后的发展情况。
|
||||
|
||||
## 总结与实践
|
||||
|
||||
今天我介绍了GraalVM中的AOT编译框架SubstrateVM。
|
||||
|
||||
SubstrateVM的设计初衷是提供一个高启动性能、低内存开销,和能够无缝衔接C代码的Java运行时。它是一个独立的运行时,拥有自己的内存管理等组件。
|
||||
|
||||
SubstrateVM要求所要AOT编译的目标程序是封闭的,即不能动态加载其他类库等。在进行AOT编译时,它会探索所有可能运行到的方法,并全部纳入编译范围之内。
|
||||
|
||||
SubstrateVM的启动时间和内存开销都非常少,这主要得益于在AOT编译时便已保存了已初始化好的堆快照,并支持从程序入口直接开始运行。作为对比,HotSpot虚拟机在执行main方法前需要执行一系列的初始化操作,因此启动时间和内存开销都要远大于运行在SubstrateVM上的程序。
|
||||
|
||||
Metropolis项目将运用SubstrateVM项目,逐步地将HotSpot虚拟机中的C++代码替换成Java代码,从而提升HotSpot虚拟机的可维护性,也加快新Java功能的开发效率。
|
||||
|
||||
今天的实践环节,请你参考我们官网的[SubstrateVM教程](https://www.graalvm.org/docs/examples/java-kotlin-aot/),AOT编译一段Java-Kotlin代码。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user