- 适用于 JDK 23 的 GraalVM(最新版本)
- 适用于 JDK 24 的 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 调用目标拆分实现中使用的算法。
新的实现依赖于语言实现提供信息,以告知何时特定节点变为多态或通过例如将条目添加到内联缓存来增加其“多态程度”。 此事件称为“多态专门化”。 通过在专门化完成后调用 Node.reportPolymorphicSpecialize 方法将此信息提供给运行时。
本指南解释了在调用 reportPolymorphicSpecialize
后会发生什么。 您可以在 报告多态性 指南中找到有关如何正确报告多态专门化的更多信息。
方法 #
检测合适的拆分候选对象依赖于语言报告多态专门化。 一旦报告了专门化,您就可以假设多态性来自托管新多态节点的调用目标的调用者链中的某处,并且通过拆分正确的调用目标(或调用目标)可以将此节点恢复到单态状态。
然后,您标识拆分可能导致单态化的调用目标,并将它们标记为“需要拆分”。 在进一步执行期间,如果解释器即将执行对标记为“需要拆分”的调用目标的直接调用,则该调用目标将被拆分(前提是没有阻止它的未完成因素,例如 根节点不允许拆分、AST 太大等)。 这将导致使用干净配置文件创建新的调用目标(即,所有节点都返回到未初始化状态),以专门为此调用站点重新分析,因为它是在调用此新调用目标的唯一调用站点。
以下递归算法(表示为伪代码)是用于决定哪些调用目标需要标记为“需要拆分”的方法的简化版本。 一旦调用目标的其中一个节点报告多态专门化,此算法将应用于每个调用目标。 完整的实现可以在 com.oracle.truffle.runtime.OptimizedCallTarget#maybeSetNeedsSplit
中找到。
setNeedsSplit(callTarget)
if callTarget.needsSplit
return false
if sizeof(knownCallers(callTarget)) == 0
return false
if callCount(callTarget) == 1
return false
if sizeof(knownCallers(callTarget)) > 1
callTarget.needsSplit = true
else
callTarget.needsSplit = setNeedsSplit(caller(callTarget))
return callTarget.needsSplit
在伪代码的开头,您可以设置提前终止条件。 如果调用目标已标记为“需要拆分”,则无需继续。 此外,如果调用目标没有已知的调用者(例如,它是执行的“main”),则拆分不适用,因为拆分本质上与为特定调用站点复制 AST 相关联。 最后,如果这是在调用目标第一次执行期间发生的,则拆分毫无意义,因为节点的多态性是不可避免的(即,不是来自调用者,而是该调用目标的固有属性)。
在伪代码的第二部分中,区分了两种情况
1) 调用目标有多个已知的调用者 - 在这种情况下,您可以假设多态性来自多个调用者之一。 因此,您将调用目标标记为“需要拆分”。
2) 调用目标只有一个已知的调用者 - 在这种情况下,您知道将此调用目标标记为“需要拆分”无法帮助消除多态性。 但是,多态性可能来自此调用目标的唯一调用者,该调用者可能有多个调用者,并且可能是拆分的候选对象。 因此,您将递归地将算法应用于我们调用目标的调用者。
暂时忽略算法的返回值及其用法,并考虑以下 SimpleLanguage 示例来说明为什么需要这种一个调用者和多个调用者之间的区别
function add(arg1, arg2) {
return arg1 + arg2;
}
function double(arg1) {
return add(arg1, arg1);
}
function callsDouble() {
double(1);
double("foo");
}
function main() {
i = 0;
while (i < 1000) {
callsDouble();
}
}
在此示例中,表示 add
函数中 +
的节点将在 double
使用字符串参数 "foo"
调用时变为多态,这将被报告给运行时,我们的算法将应用于 add
。 所有提前返回检查都将失败(add
未标记为“需要拆分”,它有已知的调用者,这不是其第一次执行)。 观察到 add
只有一个调用者(double
),因此您将算法应用于 double
。 提前返回都失败,并且由于 double
有多个调用者,因此您将其标记为“需要拆分”,并且在后面的迭代中,对 double
的调用将被拆分,从而导致运行时状态的以下代码表示
function add(arg1, arg2) {
return arg1 + arg2; // + is polymorphic
}
function double(arg1) {
return add(arg1, arg1);
}
function doubleSplit1(arg1) {
return add(arg1, arg1);
}
function doubleSplit2(arg1) {
return add(arg1, arg1);
}
function callsDouble() {
doubleSplit1(1);
doubleSplit2("foo");
}
function main() {
i = 0;
while (i < 1000) {
callsDouble();
}
}
如您所见,多态性的来源已拆分,但这并没有解决问题,因为两个切片仍然调用相同的 add
函数,并且多态性仍然存在。 这就是算法的返回值发挥作用的地方。 如果算法成功地找到了要标记的目标,那么该目标的所有传递调用者也需要标记为“需要拆分”。 完成此最终步骤后,我们之前示例中拆分方法的最终运行时结果可以表示为以下源代码
function add(arg1, arg2) {
return arg1 + arg2; // + is polymorphic
}
function addSplit1(arg1, arg2) {
return arg1 + arg2;
}
function addSplit2(arg1, arg2) {
return arg1 + arg2;
}
function double(arg1) {
return add(arg1, arg1);
}
function doubleSplit1(arg1) {
return addSplit1(arg1, arg1);
}
function doubleSplit2(arg1) {
return addSplit2(arg1, arg1);
}
function callsDouble() {
doubleSplit1(1);
doubleSplit2("foo");
}
function main() {
i = 0;
while (i < 1000) {
callsDouble();
}
}
需要在此处观察的最后一点是,拆分不会删除原始调用目标,并且它们在配置文件中仍然具有多态性。 因此,即使为这些调用目标创建了新的调用,它们也将被拆分。 考虑如果之前示例的 main
如下所示。
function main() {
i = 0;
while (i < 1000) {
callsDouble();
}
add(1,2); // this line was added
}
一旦执行到达新添加的行,您不希望它使用具有多态 +
的 add
函数,因为此处的参数不值得多态性。 幸运的是,由于 add 已经标记为“需要拆分”,因此它将在整个执行期间保持这样,并且对 add
的最后一次调用将导致 add
函数的另一次拆分。