- 适用于 JDK 24 的 GraalVM(最新)
- 适用于 JDK 25 的 GraalVM(早期访问)
- 适用于 JDK 21 的 GraalVM
- 适用于 JDK 17 的 GraalVM
- 存档
- 开发构建
- Truffle 语言实现框架
- Truffle 分支插桩
- 动态对象模型
- 静态对象模型
- 解释器代码的主机优化
- Truffle 函数内联方法
- 分析 Truffle 解释器
- Truffle 互操作 2.0
- 语言实现
- 使用 Truffle 实现新语言
- Truffle 语言和工具迁移到 Java 模块
- Truffle 原生函数接口
- 优化 Truffle 解释器
- 选项
- 栈上替换
- Truffle 字符串指南
- 特化直方图
- 测试 DSL 特化
- 基于多语言 API 的 TCK
- Truffle 编译队列方法
- Truffle 库指南
- Truffle AOT 概述
- Truffle AOT 编译
- 辅助引擎缓存
- Truffle 语言安全点教程
- 单态化
- 拆分算法
- 单态化用例
- 向运行时报告多态特化
解释器 Java 代码的主机编译
在本文档中,我们将明确区分主机编译和客户编译。
- 主机编译应用于解释器的 Java 实现。如果解释器运行在 HotSpot 上,当 Truffle 解释器作为 Java 应用程序进行 JIT 编译(或动态编译)时,会应用这种编译。在生成原生镜像时,这种编译会提前应用。
- 客户编译应用于客户语言代码。这种编译使用部分求值(Partial Evaluation)和 Futamura 投影从 Truffle AST 和字节码中派生出优化代码。
本节讨论应用于 Truffle AST 和字节码解释器的特定领域主机编译。
主机内联 #
Truffle 解释器旨在通过应用第一次 Futamura 投影来支持运行时编译。运行时可编译的代码,也常被称为部分可求值代码,具有以下特点:
- 它天生为高性能而设计,因为它也定义了语言在运行时编译后的性能。
- 它旨在避免递归,因为递归代码无法快速进行部分求值。
- 它避免复杂的抽象和第三方代码,因为它们通常不是为 PE (Partial Evaluation) 设计的。
- 部分可求值代码的边界由使用
@TruffleBoundary
注解的方法、由对CompilerDirectives.transferToInterpreter()
的调用主导的代码块,或由对CompilerDirectives.inInterpreter()
的调用保护的代码块可靠地定义。
Truffle 主机内联利用这些特性,尽可能在主机编译期间对运行时可编译的代码路径强制执行内联。普遍的假设是,对运行时编译重要的代码对解释器执行同样重要。每当检测到 PE 边界时,主机内联阶段将不再进行任何内联决策,而是将它们推迟到更适合常规 Java 代码的后续内联阶段。
此阶段的源代码可在 HostInliningPhase 中找到。
Truffle 主机内联在编译使用 @HostCompilerDirectives.BytecodeInterpreterSwitch
注解的方法时应用。此类方法的最大节点成本可通过以下方式配置:对于原生镜像使用 -H:TruffleHostInliningByteCodeInterpreterBudget=100000
,在 HotSpot 上使用 -Djdk.graal.TruffleHostInliningByteCodeInterpreterBudget=100000
。如果一个用 @BytecodeInterpreterSwitch
注解的方法调用了另一个具有相同注解的方法,那么只要这两个方法的成本不超过预算,该方法就会被直接内联。换句话说,任何此类方法都会被内联阶段视为根字节码切换方法的一部分。这允许字节码解释器切换在需要时由多个方法组成。
原生镜像在封闭世界分析期间,会计算所有可达的运行时编译方法。任何可能从 RootNode.execute(...)
可达的方法都被确定为运行时可编译的。对于原生镜像,除了字节码解释器切换之外,所有运行时可编译方法都使用 Truffle 主机内联进行优化。此类内联过程的最大节点成本可以通过 -H:TruffleHostInliningBaseBudget=5000
进行配置。在 HotSpot 上,运行时可编译方法的集合是未知的。因此,在 HotSpot 上,我们只能依赖常规 Java 方法内联来处理未注解为字节码解释器切换的方法。
每当达到编译单元的最大预算时,内联将停止。在内联期间,相同的预算将用于探索子树。如果一个调用无法在预算内完全探索和内联,则不对单个子树做出决策。对于绝大多数运行时可编译方法,这个限制不会达到,因为有自然的 PE 边界以及对 @Child
节点执行方法的多态调用来阻止。如果存在超出预算限制的方法,建议通过添加更多 PE 边界来优化这些节点。如果一个方法超出限制,则该代码很可能在运行时编译方面也具有高成本。
调试主机内联 #
此阶段执行的内联决策,对于原生镜像最好使用 -H:Log=HostInliningPhase,~CanonicalizerPhase,~GraphBuilderPhase
进行调试,在 HotSpot 上则使用 -Djdk.graal.Log=HostInliningPhase,~CanonicalizerPhase,~GraphBuilderPhase
。你可以使用 -Djdk.graal.LogFile=FILE
将输出重定向到文件(两者都适用)。
考虑以下示例,它展示了 Truffle 解释器中先前描述的部分可求值代码的常见模式
class BytecodeNode extends Node {
@CompilationFinal(dimensions = 1) final byte[] ops;
@Children final BaseNode[] polymorphic = new BaseNode[]{new SubNode1(), new SubNode2()};
@Child SubNode1 monomorphic = new SubNode1();
BytecodeNode(byte[] ops) {
this.ops = ops;
}
@BytecodeInterpreterSwitch
@ExplodeLoop(kind = LoopExplosionKind.MERGE_EXPLODE)
public void execute() {
int bci = 0;
while (bci < ops.length) {
switch (ops[bci++]) {
case 0:
// regular operation
add(21, 21);
break;
case 1:
// complex operation in @TruffleBoundary annotated method
truffleBoundary();
break;
case 2:
// complex operation protected behind inIntepreter
if (CompilerDirectives.inInterpreter()) {
protectedByInIntepreter();
}
break;
case 3:
// complex operation dominated by transferToInterpreter
CompilerDirectives.transferToInterpreterAndInvalidate();
dominatedByTransferToInterpreter();
break;
case 4:
// first level of recursion is inlined
recursive(5);
break;
case 5:
// can be inlined is still monomorphic (with profile)
monomorphic.execute();
break;
case 6:
for (int y = 0; y < polymorphic.length; y++) {
// can no longer be inlined (no longer monomorphic)
polymorphic[y].execute();
}
break;
default:
// propagates transferToInterpeter from within the call
throw CompilerDirectives.shouldNotReachHere();
}
}
}
private static int add(int a, int b) {
return a + b;
}
private void protectedByInIntepreter() {
}
private void dominatedByTransferToInterpreter() {
}
private void recursive(int i) {
if (i == 0) {
return;
}
recursive(i - 1);
}
@TruffleBoundary
private void truffleBoundary() {
}
abstract static class BaseNode extends Node {
abstract int execute();
}
static class SubNode1 extends BaseNode {
@Override
int execute() {
return 42;
}
}
static class SubNode2 extends BaseNode {
@Override
int execute() {
return 42;
}
}
}
我们可以在 Graal 仓库中(参见类 HostInliningBytecodeInterpreterExampleTest
)通过在 graal/compiler
中运行以下命令行来将其作为单元测试运行
mx unittest -Djdk.graal.Log=HostInliningPhase,~CanonicalizerPhase,~GraphBuilderPhase -Djdk.graal.Dump=:3 HostInliningBytecodeInterpreterExampleTest
这将打印
[thread:1] scope: main
[thread:1] scope: main.Testing
Context: HotSpotMethod<HostInliningBytecodeInterpreterExampleTest$BytecodeNode.execute()>
Context: StructuredGraph:1{HotSpotMethod<HostInliningBytecodeInterpreterExampleTest$BytecodeNode.execute()>}
[thread:1] scope: main.Testing.EnterpriseHighTier.HostInliningPhase
Truffle host inlining completed after 2 rounds. Graph cost changed from 136 to 137 after inlining:
Root[jdk.graal.compiler.truffle.test.HostInliningBytecodeInterpreterExampleTest$BytecodeNode.execute]
INLINE jdk.graal.compiler.truffle.test.HostInliningBytecodeInterpreterExampleTest$BytecodeNode.add(int, int) [inlined 2, monomorphic false, deopt false, inInterpreter false, propDeopt false, subTreeInvokes 0, subTreeCost 8, incomplete false, reason null]
CUTOFF jdk.graal.compiler.truffle.test.HostInliningBytecodeInterpreterExampleTest$BytecodeNode.truffleBoundary() [inlined -1, monomorphic false, deopt false, inInterpreter false, propDeopt false, subTreeInvokes 1, subTreeCost 0, incomplete false, reason truffle boundary]
INLINE com.oracle.truffle.api.CompilerDirectives.inInterpreter() [inlined 0, monomorphic false, deopt false, inInterpreter false, propDeopt false, subTreeInvokes 0, subTreeCost 6, incomplete false, reason null]
CUTOFF jdk.graal.compiler.truffle.test.HostInliningBytecodeInterpreterExampleTest$BytecodeNode.protectedByInIntepreter() [inlined -1, monomorphic false, deopt false, inInterpreter true, propDeopt false, subTreeInvokes 1, subTreeCost 0, incomplete false, reason protected by inInterpreter()]
INLINE com.oracle.truffle.api.CompilerDirectives.transferToInterpreterAndInvalidate() [inlined 3, monomorphic false, deopt true, inInterpreter false, propDeopt false, subTreeInvokes 0, subTreeCost 32, incomplete false, reason null]
INLINE com.oracle.truffle.api.CompilerDirectives.inInterpreter() [inlined 3, monomorphic false, deopt true, inInterpreter false, propDeopt false, subTreeInvokes 0, subTreeCost 6, incomplete false, reason null]
CUTOFF com.oracle.truffle.runtime.hotspot.AbstractHotSpotTruffleRuntime.traceTransferToInterpreter() [inlined -1, monomorphic false, deopt true, inInterpreter true, propDeopt false, subTreeInvokes 0, subTreeCost 0, incomplete false, reason dominated by transferToInterpreter()]
CUTOFF jdk.graal.compiler.truffle.test.HostInliningBytecodeInterpreterExampleTest$BytecodeNode.dominatedByTransferToInterpreter() [inlined -1, monomorphic false, deopt true, inInterpreter false, propDeopt false, subTreeInvokes 0, subTreeCost 0, incomplete false, reason dominated by transferToInterpreter()]
INLINE jdk.graal.compiler.truffle.test.HostInliningBytecodeInterpreterExampleTest$BytecodeNode.recursive(int) [inlined 4, monomorphic false, deopt false, inInterpreter false, propDeopt false, subTreeInvokes 1, subTreeCost 20, incomplete false, reason null]
CUTOFF jdk.graal.compiler.truffle.test.HostInliningBytecodeInterpreterExampleTest$BytecodeNode.recursive(int) [inlined -1, monomorphic false, deopt false, inInterpreter false, propDeopt false, subTreeInvokes 1, subTreeCost 0, incomplete false, reason recursive]
INLINE jdk.graal.compiler.truffle.test.HostInliningBytecodeInterpreterExampleTest$BytecodeNode$SubNode1.execute() [inlined 1, monomorphic false, deopt false, inInterpreter false, propDeopt false, subTreeInvokes 0, subTreeCost 6, incomplete false, reason null]
CUTOFF jdk.graal.compiler.truffle.test.HostInliningBytecodeInterpreterExampleTest$BytecodeNode$BaseNode.execute() [inlined -1, monomorphic false, deopt false, inInterpreter false, propDeopt false, subTreeInvokes 1, subTreeCost 0, incomplete false, reason not direct call: no type profile]
CUTOFF com.oracle.truffle.api.CompilerDirectives.shouldNotReachHere() [inlined -1, monomorphic false, deopt false, inInterpreter false, propDeopt true, subTreeInvokes 0, subTreeCost 98, incomplete false, reason propagates transferToInterpreter]
请注意,我们还使用了 -Djdk.graal.Dump=:3
选项,它会将图发送到任何正在运行的 IdealGraphVisualizer
实例以供进一步检查。在原生镜像中,使用 -H:Dump=:2 -H:MethodFilter=...
可以转储给定方法的主机编译图。
要调试不完全探索的 CUTOFF 决策(日志中带有 incomplete true
的条目),请使用 -Djdk.graal.TruffleHostInliningPrintExplored=true
选项以在日志中查看所有不完全的子树。
调优主机内联 #
在了解如何调试和跟踪主机内联决策后,是时候看看如何对其进行调优了。第一步,需要识别对解释器良好性能至关重要的编译单元。为此,可以通过将 engine.Compilation
标志设置为 false
来以仅解释器模式执行 Truffle 解释器。之后,可以使用 Java 分析器来识别执行中的热点。有关分析的更多详细信息,请参见 Profiling.md 如果您正在寻找关于如何以及何时优化 Truffle 解释器的建议,请参见 Optimizing.md
在识别出热点方法后,例如 Truffle 字节码解释器中的字节码分派循环,我们可以使用上一节中描述的主机内联日志进行进一步调查。感兴趣的条目以 CUTOFF
为前缀,并包含一个 reason
来解释单个截断的原因。
CUTOFF
条目的常见原因有:
dominated by transferToInterpreter()
或protected by inInterpreter()
:这意味着该调用是在慢路径中执行的。主机内联不会对此类调用做出决策,只会将其标记为 CUTOFF。target method not inlinable
(目标方法不可内联):这种情况发生在无法内联的主机 VM 方法上。通常我们对此无能为力。Out of budget
(超出预算):内联此方法的预算已用尽。当方法的成本过高时会发生这种情况。
此外,为避免代码大小的膨胀,主机内联内置了一种启发式算法,用于检测被认为过于复杂而无法内联的调用子树。例如,跟踪可能会打印以下内容:
CUTOFF com.oracle.truffle.espresso.nodes.BytecodeNode.putPoolConstant(VirtualFrame, int, char, int) [inlined -1, explored 0, monomorphic false, deopt false, inInterpreter false, propDeopt false, graphSize 1132, subTreeCost 5136, invokes 1, subTreeInvokes 12, forced false, incomplete false, reason call has too many fast-path invokes - too complex, please optimize, see truffle/docs/HostOptimization.md
这表明子树中存在过多的快路径调用(默认为 10 个),达到该数量后也会停止探索。可以提供 -Djdk.graal.TruffleHostInliningPrintExplored=true
标志来查看决策的整个子树。以下调用被视为快路径调用:
- 目标方法由
@TruffleBoundary
注解的调用。 - 多态调用或没有单态分析反馈的调用。例如,对子表达式执行方法的调用。
- 递归调用。
- 本身过于复杂的调用。例如,包含过多快路径调用的调用。
以下调用不被视为快路径调用:
- 可以使用主机内联启发式算法进行内联的调用。
- 慢路径中的调用,例如任何由
transferToInterpreter()
主导或由isInterpreter()
保护的调用。 - 由于主机 VM 限制而无法内联的调用,例如对
Throwable.fillInStackTrace()
的调用。 - 不再可达的调用。
完全避免快路径调用是不可能的,例如,子节点需要在 AST 中执行。理论上,字节码解释器中可以避免所有快路径调用。实践中,语言会依赖于对运行时的 @TruffleBoundary
来实现更复杂的字节码。
在以下章节中,我们将讨论如何改进主机解释器代码的技术
优化:使用 @HostCompilerDirectives.InliningCutoff 手动截断代码路径 #
如前一节所述,启发式算法会自动截断包含过多调用的内联子树。一种优化方法是使用 @InliningCutoff 注解。
考虑以下示例:
abstract class AddNode extends Node {
abstract Object execute(Object a, Object b);
@Specialization int doInt(int a, int b) { return a + b; }
@Specialization double doDouble(double a, double b) { return a + b; }
@Specialization double doGeneric(Object a, Object b, @Cached LookupAndCallNode callNode) {
return callNode.execute("__add", a, b);
}
}
在这个例子中,doInt
和 doDouble
专用化非常简单,但还有一个 doGeneric
专用化,它会调用复杂的查找链。假设 LookupAndCallNode.execute
是一个非常复杂的方法,包含十多个快路径子树调用,我们不能指望 execute
方法被内联。主机内联目前不支持自动组件分析;但是可以使用 @InliningCutoff
注解手动指定
abstract class AddNode extends Node {
abstract Object execute(Object a, Object b);
@Specialization int doInt(int a, int b) { return a + b; }
@Specialization double doDouble(double a, double b) { return a + b; }
@HostCompilerDirectives.InliningCutoff
@Specialization double doGeneric(Object a, Object b, @Cached LookupAndCallNode callNode) {
return callNode.execute("__add__", a, b);
}
}
更改代码后,如果 AddNode
的 execute
方法符合主机内联预算,主机内联现在可能会决定内联该方法,但会在 doGeneric(...)
方法调用处强制执行 CUTOFF
。有关使用此注解的其他用例,请参阅 javadoc。
优化:从部分求值期间折叠的分支中消除重复调用 #
以下是一个示例,其中代码对于使用部分求值进行编译是高效的,但对于主机编译而言并不理想。
@Child HelperNode helperNode;
final boolean negate;
// ....
int execute(int argument) {
if (negate) {
return helperNode.execute(-argument);
} else {
return helperNode.execute(argument);
}
}
当使用部分求值编译此代码时,由于 negate
字段是编译时 final,条件保证会折叠为单一情况,因此代码是高效的。在主机优化期间,negate
字段不是编译时 final,编译器会内联两次代码或者决定不内联 execute
方法。为了避免这种情况,代码可以重写如下:
@Child HelperNode helperNode;
final boolean negate;
// ....
int execute(int argument) {
int negatedArgument;
if (negate) {
negatedArgument = -argument;
} else {
negatedArgument = argument;
}
return helperNode.execute(negatedArgument);
}
如果使用许多具有相同方法体的专用化,类似的代码模式可能会通过代码生成间接产生。主机编译器通常难以自动优化此类模式。
优化:将复杂的慢路径代码提取到单独的方法中 #
考虑以下示例:
int execute(int argument) {
if (argument == 0) {
CompilerDirectives.transferToInterpeterAndInvalidate();
throw new RuntimeException("Invalid zero argument " + argument);
}
return argument;
}
Java 编译器生成与以下代码等效的字节码:
int execute(int argument) {
if (argument == 0) {
CompilerDirectives.transferToInterpeterAndInvalidate();
throw new RuntimeException(new StringBuilder("Invalid zero argument ").append(argument).build());
}
return argument;
}
虽然这段代码对于部分求值是高效的,但它在主机内联期间会占用不必要的空间。因此,建议为代码的慢路径部分提取一个单独的方法
int execute(int argument) {
if (argument == 0) {
CompilerDirectives.transferToInterpeterAndInvalidate();
throw invalidZeroArgument(argument);
}
return argument;
}
RuntimeException invalidZeroArgument(int argument) {
throw new RuntimeException("Invalid zero argument " + argument);
}