下一代的多语言JVM:GraalVM

GraalVM是一款高性能的可嵌入式多语言虚拟机,它能运行不同的编程语言,包括:

  • 基于JVM的语言,比如Java, Scala, Kotlin和Groovy
  • 解释型语言,比如JavaScript, Ruby, R和Python
  • LLVM支持的原生语言,比如C, C++, Rust和Swift

GraalVM能有效地支持多语言应用,你可以在一个进程里同时使用多种编程语言而不会带来明显的性能开销——这样你就可以根据具体问题来选择不同语言的解决方案了。

GraalVM的设计目标是可以在不同的环境中运行程序:在JVM中、或者编译成独立的本地镜像、亦或是将Java及本地代码模块集成为更大型的应用。本文先简单介绍下GraalVM能干什么、如何开始使用它以及有哪些需要特别关注的点。

GraalVM的组件

GraalVM是一个大型项目,它有很多可插拔的组件,这使得它几乎无所不能。随便列举几点,它可以更快地运行Java程序,可以取代Nashorn来运行Node.js程序,可以运行Ruby, Python还有R。它可以把Java程序编译成只有数M大小的可执行的本地镜像,可以在Docker容器中运行,而加载时间只有几毫秒。它可以像数据库执行存储过程那样执行JavaScript代码,却不会像数据库那样要消耗太多资源。

GraalVM是一个开源项目,绝大部分是由Java编写的,不是本地代码(native code)的专家也可以参与这个项目中来。也就是说日常用于Java开发的工具就可以用来开发GraalVM。我们先来简单看下它有哪些组件。

JIT编译器Graal

没有一款优秀的JIT编译器,很难称得上是一款高性能的虚拟机。GraalVM的核心便是Graal编译器。Graal可以当作JIT编译器来使用,也可以用作提前编译的静态编译器。

正如GraalVM中的其它组件一样,Graal也是用Java语言来编写的。常见的编译器优化它都能支持:公共表达式消除(common subexpression elimination)、无用代码消除、常量折叠等等。内联及逃逸分析算法是它的看家本领。Graal是一款积极优化的编译器,它的中间代码(或中间表示,Intermediate representation,IR)所使用的程序依赖图(program dependence graph)和HotSpot虚拟机中C2编译器所用的非常相似,尽管它们两者有着显著的不同。在编译期间,高级操作(比如加载Java字段)的中间代码会被转换为底层操作(比如读取地址+偏移量处的数据)的中间代码。而底层中间代码最终会被翻译为机器代码。除了用于分析JIT编译的标准JVM参数(-XX:+PrintCompilation, XX:+PrintAssembly等)外,GraalVM还发布了一款叫Ideal Graph Visualizer的工具,它可以用来调试及分析依赖图的转换过程。

Truffle

GraalVM下一个关键的组件便是Truffle了,它是一款编程语言的实现框架。Truffle提供了一套API,你可以用它来基于源程序的抽象语言树(AST)来开发一门语言的解释器。AST求值是程序执行的相对比较简单的方式,因此实现一个解释器要比开发一个优化的编译器要容易得多。不过Truffle可以使用Graal编译器对这些解释器进行优化,因此它们的峰值性能与传统编译器不相上下,有时甚至还要更好。

Truffle使用了一项叫部分求值(partial evaluation)的技术来编译目标语言的解释器。简单来说,给Truffle一个语言解释器和一段程序,它会为这段给定的程序生成一个定制版的解释器。它会把执行时期收集到的分析及类型信息关联到依赖树的节点上,然后使用这些分析数据来进行优化。Truffle需要运行时的编译器支持部分求值,而Graal很好地满足了这项要求。

Truffle上实现的很多语言的解释器都能给我们带来不少灵感。比如js的引擎LLVM的bitcode解释器Ruby解释器Python解释器,以及R解释器——这还只是GraalVM团队的官方项目。Github上还能找到其它语言的解释器的实现。甚至还有为了演示及教学Truffle单独创建的一门语言,你可以亲自体验下。

Truffle的精华之处在于,运行的时候所有的解释器都通过同样的协议来互相操作不同编程语言中的对象,也就是说,JavaScript, Python, Ruby等不同的编程语言或它们的组合所写出来的程序,从运行时的视角来看是没有任何区别的。运行时可以像正常优化代码那样,去优化用不同语言写出来的多语言程序,语言边界的跨越不存在任何性能开销。这就为所有生态系统下的库和模块都敞开了大门,你只需要选择最合适的语言去解决你要解决的问题就可以了,而不用为了项目所用的某个语言去专门实现一些缺少的模块。

Truffle的另一个好处是它对语言的实现进行了虚拟化,也就是说在运行时看来所有语言都是一样的。这样研发工具也可以是多语言的。比如说你可以用JavaScript的调试器来调试Ruby程序,或者使用VisualVM来分析JavaScript程序的内存使用,就像你在JavaScript,Java语言中使用这些工具一样。

本地镜像

GraalVM还有许多其它组件,比如说SubstrateVM,这是一个Java编写的小型虚拟机,它可以将Java应用编译成本地镜像。GraalVM的本地镜像不依赖于JVM来运行,也不需要加载和初始化Java类——并且启动速度还非常快。Graal编译器在生成镜像的过程中,会分析应用的类信息,并将它们提前编译成机器代码。SubstrateVM提供了所有虚拟机所应有的功能:垃圾回收,线程调度,代码缓存等等。它的代码也可以被Graal提前编译。因此它生成的可执行文件的峰值性能可能不如完全预热后的JIT编译的代码那么高,但是它的执行性能很稳定,运行时的开销也很低,并且启动时间是毫秒级的。在某些生产环境比如云或者无服务的部署中,对于长期运行的程序而言,启动时间要比对峰值性能来得重要。

除此之外,GraalVM还可以嵌入到其它的运行时平台中,对它们进行扩展以便支持多语言。目前实验版的Oracle DB就嵌入了GraalVM,你可以使用JavaScript而不是PL/SQL来编写存储过程。同样的功能也通过MySQL插件的方式提供了,因此你也可以在MySQL中使用GraalVM。这项功能看上去可能很鸡肋,不过它让你可以自由选择熟悉的编程语言或者已有的代码库和模块。

GraalVM初体验

体验GraalVM有很多种方式,这取决于你想付出多大的努力。

你当然可以从源码开始编译GraalVM,正如前面所说的,它是遵循带有类路径异常的GPL2许可(GPL2 with the classpath exception license)的开源项目——和OpenJDK相同的许可协议。不过最简单的方式还是去下载预先编译好的二进制包

发布版和JDK的功能类似,除此之外还有一个JavaScript引擎、Node.js的实现、LLVM bitcode的解释器,以及本地镜像功能。

先下载一个GraalVM的发布版,将它解压到$GRAALVM_HOME目录下。然后便可以通过GraalVM来运行java了:

> $GRAALVM_HOME/bin/java -version
java version "1.8.0_172"
Java(TM) SE Runtime Environment (build 1.8.0_172-b11)
GraalVM 1.0.0-rc3 (build 25.71-b01-internal-jvmci-0.45, mixed mode)

你也可以运行JavaScript程序——比如这个单行小程序:

> $GRAALVM_HOME/bin/js -e 'console.log(1+2)'
3

还可以在命令行下通过gu工具来安装Ruby, R或Python的试验版:

$GRAALVM_HOME/bin/gu install {ruby|python|r}

注意预编译版本的GraalVM是基于OpenJDK的。因此JDK上能运行的所有程序,GraalVM都能支持。如果你想要公平地比较下它们的性能,先通过-XX:-UseJVMCICompiler参数来关掉GraalVM中的Graal编译器,以便启用OpenJDK所用的HotSpot VM的编译器。

可以参考下入门指南看看GraalVM都能干些什么,也可以尝试下这篇文章中的实验,或者GraalVM团队收集一些用例

如果你手头已经有一个可用于性能测试的项目,并且测量GraalVM性能所有的基础设施也都就绪了,你可以尝试下不同的微基准测试工具(microbenchmarks)。比如这个。它是基于Java Microbenchmark Harness(JMH)实现的,后者也是Java微基准测试的标准工具。测量方法会连续执行一个简单的Stream API调用,对流上的数据进行计算,最后对它们进行求和。

package org.graalvm.demos;
import org.openjdk.jmh.annotations.*;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
@Warmup(iterations = 1)
@Measurement(iterations = 3)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Fork(1)
public class JavaSimpleStreamBenchmark {
  static int[] values = new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
  @Benchmark
  public int testMethod() {
    return Arrays.stream(values)
      .map(x -> x + 1)
      .map(x -> x * 2)
      .map(x -> x + 2)
      .reduce(0, Integer::sum);
  }
}

在我本机上,GraalVM的执行性能要比OpenJDK 1.8快上数倍。如果你想试一下,可以克隆下它的仓库。

git clone https://github.com/graalvm/graalvm-demos
cd graalvm-demos/java-simple-stream-benchmark

编译完后可以通过$GRAALVM_HOME/bin/java和正常的OpenJDK来分别运行一下,看看它们执行结果的区别。

mvn clean install
$GRAALVM_HOME/bin/java -jar target/benchmarks.jar

当然这并不是科学的评估性能的方法,正常应该是你自己去测试性能影响。不过这至少说明,某些代码上,GraalVM的执行速度是要远快于HotSpot VM的。

使用GraalVM来集成Java与其它语言

多语言是GraalVM最有意思的特性之一了,我们来看下它是如何实现的。

GraalVM多语言API的核心是Context类。Context代表了全局所有非Java语言(能编译成JVM字节码)的全局运行时状态。你可以根据自己的需要初始化对应的语言,并用它们来编写代码。下面的代码是GraalVM多语言程序的一个简单的例子。这是一段普通的Java代码,它会对JavaScript的字符串求值,里面声明了一个42的值,然后将一个Value对象返回给Java。

Context context = Context.create();
Value result = context.eval("js", "42");
assert result.asInt() == 42;

不同的语言是通过Value来进行对话的。任何Java对象都可以通过Value.asValue(Object value)方法来转化成Value对象,而Value对象也可以通过Value.as(Class targetType)方法来转换成对应的Java对象。具体的转换过程不在本文的讨论范围之内,不过它的API已经说的很清楚了:数值转换为数值,字符串转换为String,可执行的值转换成接口,集合转换为集合,等等。下面所有表达式的结果都是true:

context.eval("js", "'foobar'").as(String.class).equals("foobar");
context.eval("js", "{foo:'bar'}").as(Map.class).get("foo").equals("bar");
@FunctionalInterface interface IntFunction { int f(int value); }
context.eval("js", "(function(a){a})").as(IntFunction.class).f(42) == 42;

有了Context和Value,你就可以在不同语言的模块间进行数据传递了。

不过现代的应用程序通常将组件的多语言实现细节通过某种抽象给隐藏起来了。比如,这个Spring Boot web应用的例子,它通过R语言来将CPU的使用数据绘制成SVG图像。

在这个应用中,GraalVM的多语言Context定义成了Spring的一个Bean:

@Bean
public Context getGraalVMContext() {
     return Context.newBuilder().allowAllAccess(true).build();
}

R语言编写的函数会接收数据并绘制出图像(数据源在一个资源文件中),这个函数也暴露成了一个Bean。它会接收GraalVM上下文,对R语言中的source求值,将结果转换成Java的Function并返回。

@Bean
Function<Double, String> getPlotFunction(@Autowired Context ctx) {
    Source source =
        Source.newBuilder("R", rSource.getURL()).build();
    return ctx.eval(source).as(Function.class);
}

完成之后,R的函数就可以像Java的函数式接口那样使用了。类似的,你也可以把GraalVM支持的其它语言集成到你的应用中。

结论

本文介绍了GraalVM以及它的组件——Graal编译器,Truffle以及本地镜像工具,以及多语言程序中最重要的API,同时还提供了一份尽可能简单的GraalVM入门指南。

GraalVM是值得一试的。许多程序在GraalVM上都能执行得更快;它的快速启动也能让很多应用受益;而有的应用可以通过它支持的其它语言实现的模块得到增强,比如Ruby, JavaScript, R和Python等。

原文链接

原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/60363.html

(0)
上一篇 2021年8月10日
下一篇 2021年8月10日

相关推荐

发表回复

登录后才能评论