解释器 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); 
   }
}

在这个例子中,doIntdoDouble 专用化非常简单,但还有一个 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); 
   }
}

更改代码后,如果 AddNodeexecute 方法符合主机内联预算,主机内联现在可能会决定内联该方法,但会在 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);
}

联系我们