- 适用于 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 语言安全点教程
- 单态化
- 拆分算法
- 单态化用例
- 向运行时报告多态特化
Truffle 函数内联方法
Truffle 为所有使用该框架构建的语言提供了自动内联。自 20.2.0 版本以来,引入了一种新的内联方法。本文档描述了新方法的运作方式,将其与传统内联方法进行比较,并阐述了新方法的设计选择。
内联 #
内联是将函数调用替换为函数体本身的过程。这消除了调用的开销,但更重要的是,它为编译器的后期阶段开辟了更多的优化机会。该过程的缺点是编译的大小会随着每个内联函数而增长。过大的编译单元难以优化,并且用于安装代码的内存是有限的。
鉴于以上所有原因,选择哪些函数进行内联是在内联函数所预期收益与编译单元大小增加的成本之间进行微妙权衡。
Truffle 传统内联 #
Truffle 在很长一段时间内都有其内联方法。不幸的是,这种早期方法存在多个问题,主要问题在于它依赖于调用目标中 Truffle AST 节点的数量来近似调用目标的大小。
AST 节点是实际调用目标代码大小的非常糟糕的近似,因为无法保证单个 AST 节点会产生多少代码。例如,一个专门用于添加两个整数的加法节点,与该节点专门用于添加整数、双精度浮点数和字符串(更不用说不同的节点和来自不同语言的节点)相比,将产生明显更少的代码。这使得无法拥有一个能在所有 Truffle 语言中可靠工作的单一内联方法。
关于传统内联,一个值得注意的地方是,由于它只使用 AST 中的信息,所以内联决策是在部分求值开始之前做出的。这意味着我们只对决定内联的调用目标进行部分求值。这种方法的优点是不会在最终不进行内联的调用目标的部分求值上花费时间。另一方面,这导致了频繁的编译问题,这些问题源于内联器做出的糟糕决策。例如,生成的编译单元可能太大而无法编译。
语言无关的内联 #
新内联方法的主要设计目标是使用部分求值后的 Graal 节点(编译器节点)数量作为调用目标大小的代理。这是一个更好的大小代理,因为部分求值消除了 AST 的所有抽象,并生成了一个更接近调用目标实际执行的低级指令的图。这在决定是否内联调用目标时产生了更精确的成本模型,并消除了 AST 所携带的许多语言特定信息(因此得名:语言无关内联)。
这是通过对每个候选调用目标执行部分求值,然后在之后做出内联决策来实现的(与传统内联在进行任何部分求值之前做出决策不同)。将进行的部分求值的数量以及将内联的数量都由预算概念控制。这些分别称为“探索预算”和“内联预算”,两者都以 Graal 节点数量表示。
这种方法的缺点是,即使我们最终决定不内联的调用目标,我们也需要进行部分求值。这导致平均编译时间相对于传统内联有可衡量的增加(大约 10%)。
观察和影响内联 #
内联器维护一个内部调用树,以跟踪对各个目标的调用的状态,以及做出的内联决策。以下章节解释了调用树中的调用可以处于的各种状态,以及如何在编译期间找出做出了哪些决策。
调用树状态 #
内联 调用树中的节点代表对特定目标的调用。这意味着,如果一个目标两次调用另一个目标,尽管是同一个调用目标,我们也会将其视为两个节点。
每个节点可以处于以下六种状态之一:
- Inlined(已内联) - 此状态表示调用已被内联。最初,只有编译的根处于此状态,因为它是隐式“内联”的(即,是编译单元的一部分)。
- Cutoff(截止) - 此状态表示调用目标未进行部分求值,因此甚至未考虑内联。这通常是由于内联器达到了其探索预算限制。
- Expanded(已展开) - 此状态表示调用目标已进行部分求值(因此,已考虑内联),但决定不内联。这可能是由于内联预算限制,或目标被认为内联成本过高(例如,内联一个具有多个传出“截止”调用的微小目标只会给编译单元引入更多调用)。
- Removed(已移除) - 此状态表示此调用存在于 AST 中,但部分求值移除了该调用。这是相对于传统内联的优势,传统内联提前做出决策,无法注意到此类情况。
- Indirect(间接) - 此状态表示间接调用。我们无法内联间接调用。
- BailedOut(已中断) - 这种情况应该非常罕见,并被视为性能问题。它表示对目标的部分求值导致了
BailoutException
,即未能成功完成。这意味着该特定目标存在一些问题,但我们不会中止整个编译,而是将该调用视为无法内联。
追踪内联决策 #
Truffle 提供了一个引擎选项,可以在编译期间追踪调用树的最终状态,包括大量伴随数据。此选项为 TraceInlining
,可以通过所有常用方式进行设置:通过向语言启动器添加 --engine.TraceInlining=true
,如果在运行执行客户端语言(用 Truffle 实现的语言)的常规 Java 程序时,通过向命令行添加 -Dpolyglot.engine.TraceInlining=true
,或为引擎显式设置该选项。
以下是 JavaScript 函数的 TraceInlining
示例输出:
[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 节点数量。增加此预算可能会导致更多的候选被内联,从而导致更大的编译单元。这反过来可能会减慢编译速度,特别是在部分求值后的阶段,因为更大的图需要更多时间进行优化。它还可能改善性能(移除调用,优化阶段具有更宏观的视图)或损害性能,例如当图过大而无法正确优化或根本无法编译时。