读书笔记《supercharge-your-applications-with-graalvm》第 4 章 Graal 即时编译器
Chapter 4: Graal Just-In-Time Compiler
在 第 3 章中,GraalVM 架构< /em>,我们了解了 GraalVM 架构和构成它的各种组件。我们使用 Truffle 了解了 GraalVM Polyglot 架构的一些细节,并谈到了 Graal 的 just-in-time (JIT) 编译器。我们研究了 Graal JIT 如何通过实现 Java 虚拟机编译器接口插入 Java 虚拟机。在本章中,我们将通过使用 Ideal Graph Visualizer 工具运行示例代码并可视化 Graal 图和 Graal JIT 执行的优化来探索 Graal JIT 编译器的工作原理。
在本章中,我们将介绍以下主题:
- Setting up the environment
- Understanding the Graal JIT compiler
- Understanding Graal compiler optimizations
- Debugging and monitoring applications
在本章结束时,您将非常清楚 Graal JIT 编译的工作原理,了解各种优化技术,知道如何使用 Ideal Graph Visualizer 诊断和调试性能问题,并能够微调 Graal JIT编译器配置以获得最佳性能。
Technical requirements
在本章中,我们将使用一些示例代码并使用工具对其进行分析。以下是本章所需的一些工具/运行时:
- OpenJDK (https://openjdk.java.net/)
- GraalVM (https://www.graalvm.org/)
- VisualVM (https://visualvm.github.io/index.html)
- The Ideal Graph Visualizer
- There are some sample code snippets, which are available in our Git repository. The code can be downloaded from https://github.com/PacktPublishing/Supercharge-Your-Applications-with-GraalVM/tree/main/Chapter04.
- The Code in Action video for this chapter can be found at https://bit.ly/3fmPsaP.
- Seafoam (an Open Source project, which provides enhanced functionality and a good alternate tool for IGV)
Setting up the environment
在本章中,我们将使用 VisualVM 和 Ideal Graph Visualizer 来了解 Graal JIT 的工作原理。这种理解将帮助我们在后续章节中使用 Graal 构建最佳代码。
Setting up Graal
在 第 3 章中,GraalVM 架构< /em>,我们讨论了 Graal 的 两个版本 - 社区版和 企业版 (EE)。 Graal Community Edition 可以从 技术要求部分中提到的 Git 存储库下载,而 EE 需要您在 Oracle 注册才能下载。 EE 可免费用于评估和非生产应用程序。
Installing the Community Edition
要安装 GraalVM 社区版,请转到 https ://github.com/graalvm/graalvm-ce-builds/releases 并下载目标操作系统(macOS、Linux 和 Windows)的最新版本 .在编写本书时,最新版本是 21.0.0.2,包含基本的 Java 8 或 Java 11 版本。社区版基于 OpenJDK 构建。
请按照接下来为您的目标操作系统提供的说明进行操作。最新的说明可以在 https://www.graalvm.org/docs/getting-started/#install-graalvm 找到。
Installing GraalVM on macOS
对于 macOS,下载 GraalVM 存档文件后,解压缩存档并将解压缩文件夹的内容复制到 /Library/Java/JavaVirtualMachines/<graalvm>/Contents /首页。
一旦我们 复制了文件,我们必须导出路径以访问 GraalVM 二进制文件。让我们在终端上运行以下 export 命令:
导出 PATH=/Library/Java/JavaVirtualMachines/
export JAVA_HOME=/Library/Java/JavaVirtualMachines/
对于 macOS Catalina 及更高版本,需要删除 quarantine 属性。可以使用以下命令完成:
sudo xattr -r -d com.apple.quarantine <graalvm-path>
如果不这样做,您将看到以下错误消息:
图 4.1 – 在 MacOS 上运行 Graal 时出现错误消息
SDKMAN 提供了一种自动化的安装 GraalVM 的方式。请参考https://sdkman.io/了解更多详情.
Installing GraalVM on Linux
要在 Linux 上安装 GraalVM,请解压缩 下载的 zip 文件,将其复制到任何 目标文件夹,然后设置 PATH 和 JAVA_HOME 路径指向解压文件所在的文件夹。为此,请在命令行上执行以下命令:
导出 PATH=
export JAVA_HOME=<graalvm>
Installing GraalVM on Windows
要在 Windows 上安装 GraalVM,请提取 .zip 文件,将其复制到任何 目标文件夹,设置PATH和JAVA_HOME路径指向解压文件所在的文件夹。要设置 PATH 环境变量,请在终端上执行以下命令:
setx /M PATH "C:\Progra~1\Java\
setx /M JAVA_HOME "C:\Progra~1\Java\
要检查安装和设置是否完成,请在终端上运行 java -version 命令。
执行命令后,您应该会看到类似于以下输出的内容(我在 Java 11 上使用 GraalVM EE 21.0.0。您应该会看到您安装的版本):
java版本“11.0.10” 2021-01-19 LTS
Java(TM) SE 运行时环境 GraalVM EE 21.0.0 (build 11.0.10+8-LTS-jvmci-21.0-b06)
Java HotSpot(TM) 64 位服务器 VM GraalVM EE 21.0.0(内部版本 11.0.10+8-LTS-jvmci-21.0-b06,混合模式,共享)
现在让我们探索 GraalVM 安装的文件夹结构。在 GraalVM 安装文件夹中,您将找到下表中解释的文件夹结构:
在上一章中,我们详细介绍了 Graal 附带的各种运行时、工具和实用程序。 Graal Updater 是用于安装可选运行时的非常重要的工具之一。要检查可用的运行时,请执行 gu list。以下屏幕截图显示了典型输出:
图 4.2 – Graal 更新程序列表
我们可以运行 gu install <runtime> 来安装其他运行时。
Installing EE
GraalVM EE 可用于免费试用和非生产使用。它可以从 https://www.graalvm.org 下载/下载/。
选择所需的 GraalVM Enterprise 版本。该网站会将您重定向到 Oracle 的注册页面。如果您已经注册,您应该可以登录,您将被重定向到可以下载 GraalVM 和支持工具的页面。在编写本书时,屏幕类似于以下屏幕截图:
图 4.3 – GraalVM EE 下载页面
您可以选择 您想要下载的正确版本的 EE 以及基本 JDK 版本。在撰写本书时,Java 8 和 Java 11 是两个可行的版本。当您向下滚动此页面时,您将找到以下内容的下载链接:
- Oracle GraalVM Enterprise Edition Core: This the code for GraalVM.
- Oracle GraalVM Enterprise Edition Native Image: This is the native image tool. It can also be downloaded using Graal Updater later.
- Ideal Graph Visualizer: This is a very powerful Graal graph analyzer tool. It needs to be downloaded for this chapter. See the instructions in the Installing the Ideal Graph Visualizer section.
- GraalVM LLVM Toolchain: This is the LLVM toolchain, which is required if you want to compile and run C/C++ applications on GraalVM.
- Oracle GraalVM Enterprise Edition Ruby Language Plugin: This is the Ruby language compiler and runtime. It can also be downloaded using Graal Updater later.
- Oracle GraalVM Enterprise Edition Python Language Plugin: This is the Python language compiler and runtime. It can also be downloaded using Graal Updater later.
- Oracle GraalVM Enterprise Edition WebAssembly Language Plugin: This is the WebAssembly language compiler and runtime. It can also be downloaded using Graal Updater later.
- Oracle GraalVM Enterprise Edition Java on Truffle: This is the JVM implementation on the Truffle interpreter.
Switching between editions
我们可以在同一台机器上安装多个版本/发行版的 GraalVM,并且可以在这些不同发行版之间切换。在本章中,我们将在分布之间切换以比较它们的性能。在发行版之间切换的最佳方式是使用 Visual Studio Code。 Visual Studio Code 提供了一个 GraalVM 插件,帮助我们添加各种分布,只需单击一个按钮,我们就可以在各种分布之间切换。请参考https://www.graalvm.org/tools/vscode/和https://marketplace.visualstudio.com/items?itemName=oracle-labs -graalvm.graalvm 了解更多详情。有关如何安装 Visual Studio Code 并将其用于调试应用程序的更多详细信息,请参阅本章后面的调试和监视应用程序部分。
我们还可以创建 shell 脚本来通过将 PATH 和 JAVA_HOME 环境变量设置为指向适当的发行版来在各种发行版之间切换。
Installing Graal VisualVM
Java VisualVM 是用于分析应用程序的堆、线程和 CPU 利用率的最强大的工具之一。 VisualVM 广泛用于分析核心转储、堆转储和离线应用程序。它是一个非常复杂的工具,可以识别瓶颈并优化 Java 代码。
自 JDK 9 起,VisualVM 已迁移并升级为 Graal VisualVM。 Graal VisualVM 将功能扩展为包括对 Graal 进程的分析,目前支持 JavaScript、Python、Ruby 和 R。Graal VisualVM 还支持对原生图像进程的一些有限的监控和分析功能。 Graal VisualVM 与 Graal Community Edition 和 EE 捆绑在一起。 Graal VisualVM 可以在 .bin/jvisualvm 找到(.exe for windows)。
让我们快速浏览一下 Graal VisualVM 的主要功能。 Graal VisualVM 有一个非常直观的界面。主窗口的左侧面板(参见图 4.3)显示了所有 Local 和 Remote 进程。使用它,我们可以轻松地连接到这些进程以开始我们的分析:
图 4.4 – VisualVM,左窗格
一旦我们连接到进程,在 右侧面板中,我们将看到以下五个选项卡:
- Overview: On this tab, we can see the process configuration, JVM arguments, and system properties. The following screenshot shows the typical screen for the FibonacciCalculator process that we are running:
图 4.5 – VisualVM – 应用概览
- Monitor: On this tab, we can see CPU usage, heap allocation, the number of classes that are loaded, the number of threads that are running, and so on. We can also force a garbage collection to see how the process behaves. We can perform a heap dump to do a deeper analysis of the heap allocations. Here is a screenshot of the window:
图 4.6 – VisualVM – 应用程序监控
- Threads: This tab provides detailed information about the various threads that are running the processes. We can also capture a thread dump to perform further analysis. This tab not only shows the live threads, but we can also analyze the threads that have finished execution. The following screenshot shows the typical Threads tab:
图 4.7 – VisualVM – 应用程序线程
这是线程转储的典型屏幕截图,可用于识别是否存在任何死锁或线程等待:
图 4.8 – VisualVM – 线程转储
- Sampler: This tab can be used to take a snapshot of the running process and carry out analysis on CPU, memory, and so on. Here is a screenshot that shows the memory usage for the snapshot we take by clicking the Snapshots button:
图 4.9 – VisualVM – 使用快照的内存使用
- Profiler: This is like the sampler, but it runs all the time. Apart from CPU and memory, we can also look at JDBC invocations and the time it takes to get the response. The next screenshot shows CPU profiling:
图 4.10 – VisualVM – 应用程序分析器
除此之外,Graal VisualVM 可用于分析核心转储并确定任何 Java 进程崩溃的根本原因。在编写本书时,Graal VisualVM 支持 JavaScript 和 Ruby(仅限堆、对象视图和线程视图)、Python 和 R(仅限堆和对象视图)。
JDK Flight Recorder (JFR) 分析是 VisualVM 的另一个强大功能。它可以帮助我们分析由 JFR 连接的数据,而不会对正在运行的进程造成任何开销。 JFR 提供更高级的分析,包括捕获和分析文件 I/O、套接字 I/O 和除 CPU 和线程之外的线程锁。
Graal VisualVM 还提供扩展 API,因此我们可以编写自定义插件。可以使用各种插件来扩展 Graal VisualVM。以下是一些使用最广泛的插件:
- Visual GC plugin: This plugin provides a powerful interface to monitor garbage collection, class loader, and JIT compiler performance. It is a very powerful plugin that can identify optimizations in the code to improve performance.
- Tracer: Tracer provides a better user interface for detailed monitoring and analyzing of the applications.
- Startup Profiler: As the name suggests, this provides instrumentation to profile the startups and identify any optimizations that can be performed to improve the startups.
您可以在 https://visualvm.github.io/pluginscenters.html 。
Installing the Ideal Graph Visualizer
Ideal Graph Visualizer 是一个非常强大的工具,用于分析 Graal JIT 如何执行各种优化。这需要对作为中间表示的 Graal Graphs 有深入的了解。在本章后面,我们将介绍 Graal Graph 以及如何使用 Ideal Graph Visualizer,以便我们了解 Graal 如何执行各种优化。这很关键,因为它可以帮助我们编写更好的代码并在开发时优化代码,并减少编译器的负载以及时执行它。
The Ideal Graph Visualizer 可与 GraalVM EE 一起使用。它可以从 Oracle 网站下载。在将 PATH 设置为解压缩/安装的位置后,可以使用以下命令启动 Ideal Graph Visualizer:
理想图形可视化器
--jdkhome 标志可用于指向正确版本的 GraalVM。启动后,您将看到以下屏幕:
图 4.11 – Ideal Graph Visualizer – 主窗口
Ideal Graph Visualizer 需要 Graal 转储来呈现和分析 Graal 图表。可以使用以下命令创建 Graal 转储:
java -Dgraal.Dump=:n
上述命令中的n可以是1、2或3,每个数字代表一个级别冗长。这会生成一个名为 graal_dumps 的文件夹,其中包含 bgv 文件(Binary Graph Files)。有时你会发现各种bgv文件由于失效和重新编译(去优化或堆栈替换——请参考第 2 章,JIT、HotSpot 和 GraalJIT ,并找到 堆栈替换 部分了解更多信息)。这些 bgv 文件可以在 The Ideal Graph Visualizer 中打开以进行分析。 bgv 文件加载后,您将看到如下屏幕:
图 4.12 – Ideal Graph Visualizer – 主窗口 – Graal dump
左侧窗格可用于浏览编译和优化的各个阶段,主窗口显示图形,右侧窗格可用于配置如何呈现这些图形.我们可以查看 Graal Graphs、Call Graph、AST 和 Truffle Call Tree。
Ideal Graph Visualizer 也可以从 Java 运行时连接(使用 Dgraal.PrintGraph=Network 标志)以在应用程序代码执行时实时查看图形。
在下一节中,我们将探讨如何阅读这些 Graal 图表以了解 Graal 编译器的工作原理。
Understanding the Graal JIT compiler
在上一章中,我们简要介绍了 Graal 编译器及其周围的生态系统。在本节中,我们将深入研究各种编译器选项,并了解 Graal 如何及时优化代码。在下一节中,我们将了解 Ahead-of-Time 编译,以及如何创建原生镜像。在我们深入了解 Graal 编译器的工作原理之前,让我们快速了解一下 Graal 编译器的一些配置,这些配置可以作为参数传递给虚拟机。
Graal compiler configuration
Graal 编译器可以配置各种参数,这些参数可以从java命令(在java 的 GraalVM 版本)。在本节中,我们将介绍一些最有用的命令行配置。
我们将在示例应用程序上尝试这些不同的标志,以了解它如何影响 Graal 编译器。
让我们编写一个名为 FibonacciCalculator 的简单 Java 类。这是该类的源代码:
类斐波那契计算器{
public int[] findFibonacci(int count) {
int fib1 = 0;
int fib2 = 1;
int currentFib, index;
int [] fibNumbersArray = new int[count];
for(index=2; index < count; ++index ) {
currentFib = fib1 + fib2;
fib1 = fib2;
fib2 = currentFib;
fibNumbersArray[index - 1] = currentFib;
}
返回fibNumbersArray;
}
public static void main(String args[])
{
斐波那契计算器 fibCal = 新的斐波那契计算器();
long startTime = System.currentTimeMillis();
现在长 = 0;
最后一次 = startTime;
for (int i = 1000000000; i < 1000000010; i++) {
int[] fibs = fibCal.findFibonacci(i);
长总计 = 0;
for (int j=0; j
total += fibs[j];
}
now = System.currentTimeMillis();
System.out.printf("%d (%d ms)%n", i , now – last);
最后=现在;
}
long endTime = System.currentTimeMillis();
System.out.printf("总计: (%d ms)%n", System.currentTimeMillis() - startTime);
}
}
如您所见,我们正在生成1000000000到1000000010斐波那契数,然后计算所有的总和生成的斐波那契数。将代码写入循环以触发 编译阈值。
JIT 有很多优化机会。让我们先用 Java HotSpot 运行这个程序:
图 4.13 – FibonnaciCalculator – Java HotSpot 输出
如您所见,初始迭代花费的时间最多,并且在迭代中优化到大约 1,300 毫秒。现在让我们使用从 Graal EE 发行版中获得的 javac 编译代码,并使用 Graal JIT 运行相同的程序。以下屏幕截图显示了使用 GraalVM(Java 11 上的 GraalVM EE 21.0.0.2)运行相同应用程序的输出:
图 4.14 – FibonnaciCalculator – GraalVM 输出
我们可以看到 性能的显着改进。 Graal 开始时与 Java HotSpot 类似,但在迭代过程中它优化到了 852 毫秒,而运行 HotSpot 则需要 1,300 毫秒。以下选项用于禁用 GraalJIT 并在 GraalVM 上使用 HotSpot:
-XX:-使用JVMCI编译器
这通常用于比较 Graal 的性能。让我们使用 GraalVM EE 21.0.0.2 编译器使用前面的源代码运行此选项:
java -XX:-UseJVMCICompiler FibonacciCalculator/
以下是运行上述命令后的输出截图:
图 4.15 – FibonnaciCalculator – GraalVM (21/Java 11) 输出
如您所见,尽管我们使用的是 Graal 编译器,但性能与 Java HotSpot 相似,实际上比 Java HotSpot 15 慢。请注意,我们的 Graal 是在 Java 11 上运行的。
CompilerConfiguration 标志用于指定要使用的 JIT 编译器。以下是我们可以传递来设置 编译器配置的参数:
-Dgraal.CompilerConfiguration
我们有三个选择;让我们也用我们的示例代码运行这些选项,看看它是如何执行的:
- -Dgraal.CompilerConfiguration=enterprise: This uses the enterprise JIT, and generates the optimum code. However, there will be initial slowdowns due to compilation:
图 4.16 – FibonnaciCalculator – 企业编译器配置
- -Dgraal.CompilerConfiguration=community: This produces the community version of JIT, which optimizes to a decent extent. The compilation is therefore faster.
图 4.17 – FibonnaciCalculator – 社区编译器配置
- -Dgraal.CompilerConfiguration=economy: This compiles quickly, with fewer optimizations:
图 4.18 – FibonnaciCalculator – 经济编译器配置
我们可以看到在使用企业、社区和经济体时性能存在显着的差异。下面是三个选项的性能对比:
图 4.19 – FibonnaciCalculator – 企业与社区与经济配置
除此之外,还有许多其他性能调整选项可用于提高编译器的性能,例如:
-Dgraal.UsePriorityInlining(真/假)
前面的标志可用于启用/禁用高级内联算法。禁用此功能可缩短 编译时间并提高吞吐量。
此标志可用于禁用自动矢量化优化:
-Dgraal.Vectorization(真/假)
此标志可用于禁用路径复制优化,例如基于优势的复制模拟。当它被禁用时,它会对吞吐量产生影响:
-Dgraal.OptDuplication(真/假)
下一个标志可以设置为介于 -1 和 1 之间的值。当该值低于 0 时,JIT 会减少内联所花费的工作量。这将改善启动并提供吞吐量。当该值大于 0 时,JIT 会花费更多的精力进行内联,从而提高性能:
-Dgraal.TuneInlinerExploration(-1 到 +1)
这是一个非常有用的标志,可以启用它来跟踪 JIT 编译器如何决定内联优化:
-Dgraal.TraceInlining(真/假)
当我们为示例代码启用此标志时,我们得到以下信息:
FibonacciCalculator.main(String[]) 的编译:
在 FibonacciCalculator.main(FibonacciCalculator.java:20) [bci: 4]: <GraphBuilderPhase> FibonacciCalculator.<init>():是的,内联方法
在 FibonacciCalculator.main(FibonacciCalculator.java:25) [bci: 32]: <GraphBuilderPhase> FibonacciCalculator.findFibonacci(int):不,字节码解析器没有替换调用
FibonacciCalculator.main(String[]) 的编译:
在 FibonacciCalculator.main(FibonacciCalculator.java:20) [bci: 4]: <GraphBuilderPhase> FibonacciCalculator.<init>():是的,内联方法
在 FibonacciCalculator.main(FibonacciCalculator.java:25) [bci: 32]:
├──<GraphBuilderPhase> FibonacciCalculator.findFibonacci(int):不,字节码解析器没有替换调用
└──<PriorityInliningPhase> FibonacciCalculator.findFibonacci(int):是的,根据成本效益分析值得内联。
java.lang.String.hashCode() 的编译:
at java.lang.String.hashCode(String.java:1504) [bci: 19]:
├──<GraphBuilderPhase> java.lang.String.isLatin1():不,字节码解析器没有替换调用
└──<PriorityInliningPhase> java.lang.String.isLatin1():是的,预算足够大,可以内联这个调用站点。
at java.lang.String.hashCode(String.java:1504) [bci: 29]:
├──<GraphBuilderPhase> java.lang.StringLatin1.hashCode(byte[]): 不,字节码解析器没有替换调用
└──<PriorityInliningPhase> java.lang.StringLatin1.hashCode(byte[]):是的,预算足够大,可以内联这个调用站点。
甚至可以为其他 GraalVM 启动器设置这些优化标志,例如 js(用于 JavaScript)、node 和 lli。
Graal JIT compilation pipeline and tiered optimization
在上一章的Graal JIT 编译器部分,我们了解了 Graal JIT 如何通过 JVMCI 与虚拟机集成。在本节中,让我们更深入地了解 Graal JIT 是如何与虚拟机交互的。
Graal 对 代码进行了三层优化。分层方法 帮助 Graal 执行从更多独立于平台的表示(高级中间表示)到更多依赖于平台的表示(低级中间表示)的优化。下图显示了 Graal JIT 如何与虚拟机交互并执行这三层优化:
图 4.20 – Graal JIT 编译器 – 编译层
- The virtual machine passes the bytecode and metadata to the Graal JIT when it hits the compilation threshold (refer to Chapter 2, JIT, HotSpot, and GraalJIT, to find out more about the compilation thresholds).
- Graal parses the bytecode and generates a high-level intermediate representation (HIR).
- It then performs various optimizations on the HIR. These are some of the standard Java optimization techniques that are applied, with some new techniques that have been introduced in Graal, such as partial escape analysis and advanced inlining techniques.
- Once these high-level optimizations are performed, Graal starts converting the high-level operations to low-level operations. This phase is called lowering. There are two tiers of optimizations that it performs during this phase, and it eventually generates the low-level intermediate representation (LIR) for the target processor architecture.
- Once all the optimizations are performed on the LIR, the final optimized machine code is generated and stored in the code cache, along with the reference maps that the garbage collector will use and the metadata that will be required for deoptimization.
在本节中,我们了解了 Graal JIT 编译器的内部工作原理,并探讨了会影响编译器性能的各种编译器配置。现在让我们更好地理解 Graal 中间表示。
Graal intermediate representation
中间表示(IRs)是编译器设计最重要的数据结构之一。 IR 提供了一个图表,可帮助编译器了解代码结构、识别机会并执行优化。选择正确类型的数据结构和 IR 是编译器识别 这些优化机会的关键。在 GraalVM 中,除了 JVM 中存在的 C1 和 C2 编译器外,Graal 编译器还引入了新的 IR,它基于有向图数据结构。图 中的每个节点以 Static Single Assignment (SSA) 形式表示一个值。由于 Graal 编译器完全建立在 Java 之上,节点类型是声明性的类定义,每个节点都有一个 Java 类定义,节点类型都定义为 Java 类的层次结构。表示为节点的操作和值由它们对应的类型定义,例如AddNode、IfNode和SwitchNode,所有其中派生自基类Node。边(操作数)表示为类的字段。下图显示了各种类型节点的层次结构:
图 4.21 – Graal Graph 节点 – 类层次结构
SSA 中的代码表示可以为每个值创建单个版本的变量。这有助于执行更好的数据流分析和优化。 phi 函数 (Φ) 用于转换基于决策的控制路径(例如 if 和 switch)。 Phi函数是两个值的函数,根据控制流选择值。有关更多详细信息,请参阅以下有关 SSA 的论文:https://gcc.gnu.org/onlinedocs/gccint/SSA.html 和 https://en.wikipedia.org/wiki/Static_single_assignment_form。关键在于将完整的程序转换为 SSA 以执行优化。
Graal IR 构建为 Graal 图,其中每个节点都有指向创建操作数的节点的输入边和显示控制流的后继边。后继边指向在控制流方面接续当前节点的 节点。
为了演示到目前为止我们讨论的所有内容,让我们使用 The Ideal Graph Visualizer 分析一些简单的 Java 代码。这段代码中的逻辑可能不会生成一个简单的图表——代码故意保持简单。这些循环用于达到阈值,因此当 JVM 达到阈值时,它将执行 Graal JIT 编译,如下所示:
公共类 DemostrateGraalGraph {
public long calculateResult() {
长结果 = 0;
for (int i=0; i<2000; i++) {
结果=结果+i;
}
返回结果;
}
public static void main(String[] args) {
DemonstrateGraalGraph obj = 新的DemonstrateGraalGraph();
while (true) {
//这个循环只是为了达到编译器阈值
长结果 = obj.calculateResult();
System.out.println("总计:" + 结果);
}
}
}
现在让我们使用 javac DemonstrateGraalGraph.java 命令编译前面的代码。为了保持图表简单,我们将使用 -XX:CompileOnly=DemonstrateGraalGraph:calculateResult 标志仅编译 calculateResult() 方法。我们还可以使用以下标志禁用 一些优化:
-Dgraal.FullUnroll=false、-Dgraal.PartialUnroll=false、-Dgraal.LoopPeeling=false、-Dgraal.LoopUnswitch= false、-Dgraal.OptScheduleOutOfLoops=false 和 -Dgraal.VectorizeLoops=false
所以,我们得到以下信息:
java -XX:CompileOnly=DemonstrateGraalGraph::calculateResult \
-XX:-UseOnStackReplacement \
-Dgraal.Dump=:1\
-XX:+打印编译\
-Dgraal.FullUnroll=false \
-Dgraal.PartialUnroll=false \
-Dgraal.LoopPeeling=false \
-Dgraal.LoopUnswitch=false \
-Dgraal.OptScheduleOutOfLoops=false \
-Dgraal.VectorizeLoops=false \
演示GraalGraph
这将创建一个名为 graal_dumps 的文件夹,其中包含所有 Graal JIT 活动的转储。加载 Graal 生成的 bgv 文件后,您会发现左侧窗格中列出了各个优化层,如下面的屏幕截图所示:
图 4.22 – 理想图形可视化工具 – DemonstrateGraalGraph – 左窗格
当你点击右侧页面的0: After parsing时,你会看到解析完字节码后的Graal图表示。在图中,红线代表控制流,蓝线代表数据流。控制流必须从上到下读取,但数据流通常可以通过向上读取来理解。让我们理解这张图,并与代码进行比较。请注意,此图仅适用于 calculateResult() 方法,因为我们要求 JVM 创建仅编译 calculateResult() 方法。让我们更好地理解这张图:
图 4.23 – The Ideal Graph Visualizer – DemonstrateGraalGraph – Graal Graph 解析后
程序从 0 Start 开始,循环从 7 LoopBegin 节点开始。为了使图表更易于理解,部分 部分使用标签 A 和 B 突出显示。让我们探索一下图表的这些部分是什么。
A 部分
- Section A highlights the for loop. It is converted into a 18 if statement. The input for the if statement is the current value of I, which is the output of the Phi node 9 Phi(4,22,i32) and constant 2000 node 11 C(2000) i32.
- Phi is attached where the control flows merge. In this case, 9 Phi (4,22, i32) merges the output from 4 C(0) i32 (i=0 in the for loop) and the output of the 22 + node (which is i++). This node will simply output the current value of the i after incrementing by the value of the 21 C(1) i32 node.
- This then flows into the 12 < node and is compared with 11 C(2000) i32 (which is the maximum value of the loop), and this expression is evaluated by control flow node 18 if.
B 部分
- Section B highlights the section where the result is calculated.
- The initial value of the result is represented as C (0) i64. It is i64, as we declared it as a long.
- The 8 Phi(3, 20, i64) node merges the control flow to calculate the result = result + i expression. The value of i is flowing from the 19 SignExtend node, which is an output of the current value of I, which flows from 9 Phi(4,22, i32).
- The final out flows into 24 Return when the loop ends at 18 if.
现在我们可以通过在左窗格中选择阶段来查看优化的每个阶段,以查看代码是如何优化的。让我们快速看一下这张图是如何通过各个阶段进行转换的。当我们在左侧窗格的 Outline 窗口中选择 Before Phase Lowering 时,我们将看到下图:
图 4.24 – 理想图形可视化工具 – DemonstrateGraalGraph – 降低之前的 Graal 图形
在这个阶段,我们可以看到以下优化:
- The 19 Sign Extend node is replaced with 27 Zero Extend, as the compiler found out that it is an unsigned integer. Operations with unsigned integers are less expensive than operations with signed integers.
- The 12 < node is replaced with 26 |<|, which is an unsigned less than operation, which is faster. The compiler arrives at this conclusion based on the various iterations and profiling. Since the operands are considered unsigned, even the operations are considered unsigned.
- The graph also illustrates application of the canonicalization technique of replacing <= with <, to speed up the if (which is originally the for loop) statements.
后续阶段(高层、中层和低层)可能不会显示出显着的优化,因为代码相对简单,我们已禁用了一些优化以保留图表易于阅读和理解:
图 4.25 – 理想图形可视化工具 – DemonstrateGraalGraph – Graal Graph,其他层
图 4.26 是启用了所有优化的图表。您将看到循环展开已被非常突出地用于加速循环:
图 4.26 – 理想图可视化工具 – DemonstrateGraalGraph – 最终优化图
作为 分层编译的一部分,Graal 执行各种优化。我们将在下一节中详细介绍这一点,并了解如何使用这些知识来改进我们编写代码的方式。
Understanding Graal compiler optimizations
Graal 编译器会及时对代码执行一些最高级的优化。最关键的将在以下小节中讨论。
在进入本次会议之前,请参阅 了解 JIT 执行的优化部分"ch03lvl1sec10">第 2 章,JIT、HotSpot 和 GraalJIT。
Speculative optimization
JIT 编译在很大程度上依赖于代码的运行时分析。正如我们所看到的,这些图表是基于 HotSpots 进行优化的。热点,正如我们在 第 2 章中介绍的,< em>JIT、HotSpot 和 GraalJIT,是程序经过最频繁的控制流。尝试优化整个代码是没有意义的;相反,JIT 编译器尝试优化热控制路径/流。这是基于推测和假设。当一个假设在执行过程中被证明是错误的,编译器会迅速取消优化并等待另一个机会根据新的 HotSpot 进行优化。我们在第 2 章中介绍了编译器阈值和热点、JIT、HotSpot 和 GraalJIT,在 Compiler threshold 部分。 Graal JIT 也使用类似的技术来识别热点。 Graal 执行我们在 第 2 章、JIT、Hotspot 和 GraalJIT,在了解 JIT 执行的优化部分中,还使用了一些高级技术。让我们来看看 Graal JIT 应用于代码的一些最重要的优化技术。
Partial escape analysis
在 第 2 章中,JIT、HotSpot和 GraalJIT,在标题为了解 JIT 执行的优化的部分中,我们探讨了逃逸分析。逃逸分析是最强大的技术之一。它标识了 对象的范围以及对象从本地范围逃逸到全局范围。如果它识别出没有逃逸的对象,就有机会进行优化,编译器将优化代码以使用堆栈分配而不是堆分配给本地范围内的对象。这节省了大量在堆中分配和释放内存的时间。
部分转义分析更进一步,不仅限于识别逃离方法级别范围以控制分支的对象。当发现对象仅在某些控制流中转义时,此有助于优化代码。对象未转义的其他控制流可以优化为使用本地值或标量替换。
部分转义分析查找可能通过方法调用、返回值、throw 语句等发生的转义。让我们用一个简单的代码来理解它是如何工作的:
公共无效方法(布尔标志){
Class1 object1 = new Class1();
Class2 object2 = new Class2();
//一些处理
object1.parameter = value;
//更多逻辑
如果(标志){
返回对象1;
}
返回对象2;
}
前面是一些示例代码,只是为了说明部分逃逸分析。在这段代码中,我们将 object1 创建为 Class1 的实例,并将 object2 创建为 Class2 的实例。正在进行一些处理,并且 object1 字段将更新为一些计算值。根据该标志,object1 或 object2 将转义。我们假设大多数时候flag是false,只有object1逃逸,所以每次方法创建object1都没有意义叫做。这段代码被优化为如下内容(这只是部分转义分析如何工作的说明;Graal JIT 可能不会进行这种精确的重构):
公共无效方法(布尔标志){
Class2 object2 = new Class2();
tempValue = 值;
如果(标志){
Class1 object1 = new Class1();
object1.parameter = tempValue;
返回对象1;
}
返回对象2;
}
object1 仅在需要时创建 ,并使用临时 变量来存储中间值,如果 object1 必须被初始化,那么它会在转义之前使用临时值。这优化了堆分配时间和堆大小。
Inter-procedural analysis and inlining
Graal 在 AST/Graph 级别执行 优化。这有助于 Graal 执行过程间 分析并识别任何可能永远不会为空的选项 并跳过编译该部分代码,因为它可能永远不会被调用。它为该代码块添加了一个保护,以防万一。如果控制流经该块,则 JIT 可以取消优化代码。
为了理解过程间分析和内联,一个常用的例子是 JDK 类 OptionalDouble。以下是 OptionalDouble 类的片段:
公共类 OptionalDouble {
public double getAsDouble() {
if (!isPresent) {
抛出新的 NoSuchElementException("没有 valuepresent");
}
返回值;
}
}
假设我们称之为 getAsDouble() 方法,并且该方法有一个 throw 块,但该 throw 块可能 永远不会被调用。 Graal 编译器将编译除 if 块之外的所有代码,并将放置一个 guard 语句以便 如果它被调用,它可以去优化代码。除此之外,Graal 执行更高级的内联来优化代码。我们可以通过传递 -Dgraal.Dump=:2 查看 Graal 执行的完整的 优化集。使用第 2 级的 Graal 转储,我们可以获得每个阶段的更详细的图表列表。在下一个屏幕截图中,您可以看到 Graal JIT 在各个编译层对代码执行的优化的完整列表:
图 4.27 – 理想图形可视化工具 – DemonstrateGraalGraph – 编译层
通过查看图表在每个步骤中是如何优化的,我们可以看到在开发时可以优化代码的每个区域.这 将减少 Graal JIT 的负载,代码将执行得更好。其中一些优化技术在第 2 章,JIT、HotSpot 和 GraalJIT。
Debugging and monitoring applications
GraalVM 带有一组丰富的工具,用于调试和监控应用程序。我们已经看过 VisualVM 和 Ideal Graph Visualizer。正如您在前几节中看到的,这两个工具对于详细分析非常强大。该分析还提供了有关我们如何在开发时改进代码以减少 Graal JIT 的负载以及编写高性能和低占用空间的 Java 代码的见解。除了这两个工具之外,以下是 Graal 附带的一些其他工具。
Seafoam
Seafoam 是 Ideal Graph Visualizer 的绝佳替代品。以下是 Seafoam 通过 IGV 提供的一些增强功能:
- Seafoam is a Open source project, and can be used according to MIT license.
- It provides various export options of the graphs, which is a great convince over IGV.
- It provides a CLI, which helps in running it as part of any automation.
- Seafoam also be used asa library, to embed it as part of the application.
- The graphs generated by Seafoam are simpler and easier to understand.
有关 Seafoam 的更多最新信息,请参阅 GitHub 页面 (https://github.com/Shopify/seafoam )
Visual Studio Code extension
Visual Studio Code 扩展是 Graal 最强大的集成开发环境之一。以下屏幕截图显示了适用于 Visual Studio Code 的 GraalVM 扩展:
图 4.28 – Visual Studio Code 上的 GraalVM 环境
在上一个屏幕截图中,您可以在左侧窗格中看到已配置的所有各种 GraalVM 安装。 GraalVM的各个版本之间切换非常方便,终端和整个环境都会使用选中的GraalVM。
此扩展还使安装可选组件变得容易。我们不必手动运行 gu 命令。此扩展提供了一种简单的方法来构建、调试和运行用 Java、Python、R、Ruby 和 Polyglot(混合语言代码)编写的代码。
可以通过搜索 Graal 从 Visual Studio Code Extensions 选项卡直接安装此扩展。以下屏幕截图显示了扩展安装页面:
图 4.29 – 为 Visual Studio Code 安装 GraalVM 扩展
还有一个 GraalVM 扩展包,带有 附加功能,例如 Micronaut 框架集成和 NetBeans 语言服务器,它们提供 Java 代码完成、重构、Javadoc 集成以及许多更高级的功能。下一个屏幕截图显示了 GraalVM 扩展包的安装页面:
图 4.30 – Visual Studio Code 的 GraalVM 扩展包插件
您可以在 GraalVM 网站 扩展的更多信息"_blank">https://www.graalvm.org/tools/vscode/graalvm-extension/。
GraalVM Dashboard
GraalVM Dashboard 是一个基于网络的工具,带有,我们可以对静态和动态编译进行详细分析。这对于原生图像分析非常强大。该工具提供有关编译、可达性、可用性、分析数据、动态编译参数、反优化等的详细仪表板报告。
我们将在下一章运行这个工具,当我们创建示例代码的原生镜像并对原生镜像代码进行更详细的分析时。
Command-line tools
有两个命令行工具 可以在 Polyglot 的上下文中使用,以识别进一步优化代码的机会。我们将在第 6 章中使用这些工具, Truffle 支持多语言(多语言),用于多语言优化。以下是 GraalVM 附带的两个命令行工具:
- Profiling CLI: This tool helps to identify opportunities to optimize CPU and memory usage. Please refer to https://www.graalvm.org/tools/profiling/ for more details.
- Code Coverage CLI: This tools records and analyzes the code coverage for each execution. This is very powerful for running test cases and ensuring good code coverage. This tool can also identify possible dead code that can be eliminated, or hot code that can be optimized at development time. Please refer to https://www.graalvm.org/tools/code-coverage/ for more details.
Chrome debugger
Chrome 调试器提供 Chrome 开发人员 工具扩展来调试客户语言应用程序。通过 --inspect 选项运行应用程序时可以使用 Chrome 调试器。这有助于使用 Chrome 调试 JavaScript (Node.js)。可以从 https://developers.google.com/web/tools/ 安装扩展程序chrome-devtools/。当我们在 介绍这个工具.com/book/programming/9781800564909/9" linkend="ch09lvl1sec56">第 6 章,Truffle 支持多语言(多语言)。
Summary
在本章中,我们详细介绍了 Graal JIT 和 Ahead of Time 编译器。我们获取了一个示例代码,并查看了 Graal JIT 如何使用 Ideal Graph Visualizer 执行各种优化。我们还详细介绍了 Graal Graphs。这是非常关键的知识,将帮助您分析和识别可以在开发期间应用的优化,以在运行时加速 Graal JIT 编译。
在本章中,您详细了解了 Graal JIT 编译的内部工作原理,以及如何微调 Graal JIT。您还很好地了解了如何使用一些高级分析和诊断工具来调试 Graal JIT 编译,并确定优化代码的机会。
在下一章中,我们将更详细地了解 Graal Ahead of Time 编译。
Further reading
- Partial Escape Analysis and Scalar Replacement for Java (https://ssw.jku.at/Research/Papers/Stadler14/Stadler2014-CGO-PEA.pdf)
- Understanding Basic Graal Graphs (https://chrisseaton.com/truffleruby/basic-graal-graphs/)
- Optimizing Strategies of GraalVM (https://www.beyondjava.net/graalvm-plugin-replacement-to-jvm)
- GraalVM Enterprise Edition (EE) (https://docs.oracle.com/en/graalvm/enterprise/19/index.html)
- GraalVM documentation (https://www.graalvm.org/docs/introduction/)
- Static Single Assignment (https://gcc.gnu.org/onlinedocs/gccint/SSA.html)