- 适用于 JDK 23 的 GraalVM(最新版本)
- 适用于 JDK 24 的 GraalVM(抢先体验版)
- 适用于 JDK 21 的 GraalVM
- 适用于 JDK 17 的 GraalVM
- 存档
- 开发版本
- Truffle 语言实现框架
- Truffle 分支仪表
- 动态对象模型
- 静态对象模型
- 解释器代码的宿主优化
- Truffle 函数内联方法
- 分析 Truffle 解释器
- Truffle Interop 2.0
- 语言实现
- 使用 Truffle 实现新语言
- Truffle 语言和仪表迁移到 Java 模块
- Truffle 本地函数接口
- 优化 Truffle 解释器
- 选项
- 栈上替换
- Truffle 字符串指南
- 专门化直方图
- 测试 DSL 专业化
- 基于 Polyglot API 的 TCK
- Truffle 编译队列方法
- Truffle 库指南
- Truffle AOT 概述
- Truffle AOT 编译
- 辅助引擎缓存
- Truffle 语言安全点教程
- 单态化
- 拆分算法
- 单态化用例
- 向运行时报告多态专门化
Truffle 函数内联方法
Truffle 为所有使用该框架构建的语言提供自动内联功能。从 20.2.0 版本开始,引入了一种新的内联方法。本文档介绍了新方法的工作原理,将其与传统内联方法进行了比较,并说明了新方法的设计选择。
内联 #
内联是指将对函数的调用替换为该函数的函数体。这样做消除了调用的开销,但更重要的是,它为编译器后期的阶段提供了更多优化机会。这种方法的缺点是,每次内联函数时,编译的规模都会增大。过大的编译单元难以优化,而且安装代码的空间是有限的。
因此,选择哪些函数进行内联需要在内联函数的预期收益与增加编译单元大小的成本之间进行谨慎权衡。
Truffle 传统内联 #
Truffle 已经有一段时间使用内联方法了。不幸的是,这种早期方法存在多个问题,主要问题是它依赖于调用目标中的 Truffle AST 节点数量来近似调用目标的大小。
AST 节点是调用目标实际代码大小的非常糟糕的代理,因为无法保证单个 AST 节点将生成多少代码。例如,专门用于添加两个整数的加法节点将比相同的节点(如果专门用于添加整数、双精度数和字符串)生成的代码要少得多(更不用说不同的节点以及来自不同语言的节点)。这使得不可能使用一种对所有 Truffle 语言都可靠有效的单一内联方法。
传统内联的一个显著特征是,由于它只使用 AST 的信息,因此内联决策是在部分求值开始之前做出的。这意味着我们只对决定内联的调用目标进行部分求值。这种方法的优点是,在对最终未内联的调用目标进行部分求值时,不会浪费时间。另一方面,这会导致由于内联程序做出的糟糕决策而导致的频繁编译问题。例如,生成的编译单元可能太大而无法编译。
与语言无关的内联 #
新内联方法的主要设计目标是使用部分求值后的 Graal 节点(编译器节点)数量作为调用目标大小的代理。这是一个更好的大小代理,因为部分求值消除了 AST 的所有抽象,并生成了一个更接近调用目标实际执行的低级指令的图形。在决定是否内联调用目标时,这会导致更精确的成本模型,并且消除了 AST 中的许多语言特定信息(因此得名:与语言无关的内联)。
这是通过对每个候选调用目标执行部分求值,然后在部分求值完成后做出内联决策(与传统内联在进行任何部分求值之前做出决策相反)来实现的。部分求值的量以及内联的量都由预算的概念控制。分别是“探索预算”和“内联预算”,两者都以 Graal 节点计数表示。
这种方法的缺点是,即使对于最终决定不内联的调用目标,我们也需要进行部分求值。与传统内联相比,这会导致平均编译时间有明显的增加(大约 10%)。
观察和影响内联 #
内联程序会维护一个内部调用树来跟踪各个对目标的调用的状态,以及做出的内联决策。以下各节将解释调用树中调用可能处于的哪些状态,以及如何找出编译过程中做出的哪些决策。
调用树状态 #
内联 节点 在内联 调用树 中表示对特定目标的调用。这意味着,如果一个目标两次调用另一个目标,我们会看到两个节点,即使它们是同一个调用目标。
每个节点都可以处于以下六种状态之一。
- 已内联 - 这种状态表示该调用已被内联。最初,只有编译的根处于这种状态,因为它被隐式地“内联”(即,是编译单元的一部分)。
- 已截止 - 这种状态表示未对调用目标进行部分求值,因此甚至没有考虑内联。这通常是由于内联程序遇到了探索预算限制。
- 已扩展 - 这种状态表示已对调用目标进行了部分求值(因此,已考虑内联),但决定不内联。这可能是由于内联预算限制或目标被认为太贵而无法内联(例如,内联一个带有多个传出“已截止”调用的较小目标只会为编译单元引入更多调用)。
- 已删除 - 这种状态表示此调用存在于 AST 中,但部分求值删除了该调用。这比传统内联具有优势,因为传统内联会提前做出决策,并且无法发现这种情况。
- 间接 - 这种状态表示间接调用。我们无法内联间接调用。
- 已跳出 - 这种状态应该非常罕见,并且被认为是性能问题。它表示调用目标的部分求值导致了
BailoutException
,即无法成功完成。这意味着该特定目标存在问题,但与其退出整个编译,不如将该调用视为无法内联。
跟踪内联决策 #
Truffle 提供了一个引擎选项来跟踪编译期间调用树的最终状态,包括大量伴随数据。此选项是 TraceInlining
,可以通过以下几种常见方式设置:将 --engine.TraceInlining=true
添加到语言启动器中,在运行执行来宾语言(使用 Truffle 实现的语言)的常规 Java 程序时,将 -Dpolyglot.engine.TraceInlining=true
添加到命令行中,或者 为引擎显式设置选项。
以下是 TraceInlining
对 JavaScript 函数的示例输出。
[engine] inline start M.CollidePolygons |call diff 0.00 |Recursion Depth 0 |Explore/inline ratio 1.07 |IR Nodes 27149 |Frequency 1.00 |Truffle Callees 14 |Forced false |Depth 0
[engine] Inlined M.FindMaxSeparation <opt> |call diff -8.99 |Recursion Depth 0 |Explore/inline ratio NaN |IR Nodes 4617 |Frequency 1.00 |Truffle Callees 7 |Forced false |Depth 1
[engine] Inlined parseInt <opt> |call diff -1.00 |Recursion Depth 0 |Explore/inline ratio NaN |IR Nodes 111 |Frequency 1.00 |Truffle Callees 0 |Forced true |Depth 2
[engine] Inlined M.EdgeSeparation |call diff -3.00 |Recursion Depth 0 |Explore/inline ratio NaN |IR Nodes 4097 |Frequency 1.00 |Truffle Callees 2 |Forced false |Depth 2
[engine] Inlined parseInt <opt> |call diff -1.00 |Recursion Depth 0 |Explore/inline ratio NaN |IR Nodes 111 |Frequency 1.00 |Truffle Callees 0 |Forced true |Depth 3
[engine] Inlined parseInt <opt> |call diff -1.00 |Recursion Depth 0 |Explore/inline ratio NaN |IR Nodes 111 |Frequency 1.00 |Truffle Callees 0 |Forced true |Depth 3
[engine] Inlined parseInt <opt> |call diff -1.00 |Recursion Depth 0 |Explore/inline ratio NaN |IR Nodes 111 |Frequency 1.00 |Truffle Callees 0 |Forced true |Depth 2
[engine] Expanded M.EdgeSeparation |call diff 1.00 |Recursion Depth 0 |Explore/inline ratio NaN |IR Nodes 4097 |Frequency 1.00 |Truffle Callees 2 |Forced false |Depth 2
[engine] Inlined parseInt <opt> |call diff -1.00 |Recursion Depth 0 |Explore/inline ratio NaN |IR Nodes 111 |Frequency 1.00 |Truffle Callees 0 |Forced true |Depth 2
[engine] Inlined M.EdgeSeparation |call diff -3.00 |Recursion Depth 0 |Explore/inline ratio NaN |IR Nodes 4097 |Frequency 1.00 |Truffle Callees 2 |Forced false |Depth 2
[engine] Inlined parseInt <opt> |call diff -1.00 |Recursion Depth 0 |Explore/inline ratio NaN |IR Nodes 111 |Frequency 1.00 |Truffle Callees 0 |Forced true |Depth 3
[engine] Inlined parseInt <opt> |call diff -1.00 |Recursion Depth 0 |Explore/inline ratio NaN |IR Nodes 111 |Frequency 1.00 |Truffle Callees 0 |Forced true |Depth 3
[engine] Cutoff M.EdgeSeparation |call diff 0.01 |Recursion Depth 0 |Explore/inline ratio NaN |IR Nodes 0 |Frequency 0.01 |Truffle Callees 2 |Forced false |Depth 2
[engine] Cutoff M.FindMaxSeparation <opt> |call diff 1.00 |Recursion Depth 0 |Explore/inline ratio NaN |IR Nodes 0 |Frequency 1.00 |Truffle Callees 7 |Forced false |Depth 1
[engine] Cutoff M.FindIncidentEdge <opt> |call diff 1.00 |Recursion Depth 0 |Explore/inline ratio NaN |IR Nodes 0 |Frequency 1.00 |Truffle Callees 19 |Forced false |Depth 1
[engine] Cutoff parseInt <opt> |call diff 1.00 |Recursion Depth 0 |Explore/inline ratio NaN |IR Nodes 0 |Frequency 1.00 |Truffle Callees 0 |Forced true |Depth 1
[engine] Cutoff parseInt <opt> |call diff 0.98 |Recursion Depth 0 |Explore/inline ratio NaN |IR Nodes 0 |Frequency 0.98 |Truffle Callees 0 |Forced true |Depth 1
[engine] Cutoff A.Set <split-16abdeb5> |call diff 1.00 |Recursion Depth 0 |Explore/inline ratio NaN |IR Nodes 0 |Frequency 1.00 |Truffle Callees 0 |Forced false |Depth 1
[engine] Cutoff A.Normalize <split-866f516> |call diff 1.00 |Recursion Depth 0 |Explore/inline ratio NaN |IR Nodes 0 |Frequency 1.00 |Truffle Callees 1 |Forced false |Depth 1
[engine] Cutoff A.Set <split-1f7fe4ae> |call diff 1.00 |Recursion Depth 0 |Explore/inline ratio NaN |IR Nodes 0 |Frequency 1.00 |Truffle Callees 0 |Forced false |Depth 1
[engine] Cutoff M.ClipSegmentToLine |call diff 1.00 |Recursion Depth 0 |Explore/inline ratio NaN |IR Nodes 0 |Frequency 1.00 |Truffle Callees 2 |Forced false |Depth 1
[engine] Cutoff M.ClipSegmentToLine |call diff 1.00 |Recursion Depth 0 |Explore/inline ratio NaN |IR Nodes 0 |Frequency 1.00 |Truffle Callees 2 |Forced false |Depth 1
[engine] Cutoff A.SetV <split-7c14e725> |call diff 1.00 |Recursion Depth 0 |Explore/inline ratio NaN |IR Nodes 0 |Frequency 1.00 |Truffle Callees 0 |Forced false |Depth 1
[engine] Cutoff A.SetV <split-6029dec7> |call diff 1.00 |Recursion Depth 0 |Explore/inline ratio NaN |IR Nodes 0 |Frequency 1.00 |Truffle Callees 0 |Forced false |Depth 1
[engine] Inlined L.Set <split-2ef5921d> |call diff -3.97 |Recursion Depth 0 |Explore/inline ratio NaN |IR Nodes 205 |Frequency 1.98 |Truffle Callees 1 |Forced false |Depth 1
[engine] Inlined set <split-969378b> |call diff -1.98 |Recursion Depth 0 |Explore/inline ratio NaN |IR Nodes 716 |Frequency 1.98 |Truffle Callees 0 |Forced false |Depth 2
[engine] Inlined set |call diff -1.98 |Recursion Depth 0 |Explore/inline ratio NaN |IR Nodes 381 |Frequency 1.98 |Truffle Callees 0 |Forced false |Depth 1
[engine] inline done M.CollidePolygons |call diff 0.00 |Recursion Depth 0 |Explore/inline ratio 1.07 |IR Nodes 27149 |Frequency 1.00 |Truffle Callees 14 |Forced false |Depth 0
转储内联决策 #
通过跟踪以文本形式提供的信息也可以在 IGV 转储中找到。图形是 Graal Graphs
组的一部分,位于 Call Tree
子组中。图形显示了内联之前和之后的调用树状态。
控制内联预算 #
注意:与内联相关的预算的默认值是在仔细考虑编译时间、性能和编译器稳定性之后选择的。更改这些参数可能会影响所有这些方面。
与语言无关的内联提供了两个选项来控制编译器可以进行的探索量和内联量。分别是 InliningExpansionBudget
和 InliningInliningBudget
。两者都以 Graal 节点计数表示。它们可以像任何其他引擎选项一样进行控制(即,与“跟踪内联决策”部分中描述的方式相同)。
InliningExpansionBudget
控制内联程序在何处停止对候选者的部分求值。因此,增加此预算可能会对平均编译时间产生非常负面的影响(尤其是在进行部分求值所花费的时间方面),但可能会为内联提供更多候选者。
InliningInliningBudget
控制由于内联而导致的编译单元可以包含多少 Graal 节点。增加此预算可能会导致内联更多候选者,这会导致更大的编译单元。这反过来可能会减慢编译速度,尤其是在部分求值后的阶段,因为更大的图形需要更长时间进行优化。它也可能提高性能(删除调用,优化阶段具有更大的视野)或降低性能,例如,当图形太大而无法正确优化或根本无法编译时。