LLVM 中的垃圾回收安全点¶
状态¶
本文档描述了 LLVM 的一组扩展,用于支持垃圾回收。到目前为止,这些机制已被商业 Java 实现充分验证,其中完全重定位的收集器已使用它们发货。在某些地方可能仍然存在 bug;这些将在下面指出。
它们仍然被列为“实验性”,以表明不提供跨版本的前向或后向兼容性保证。如果您的用例需要某种形式的前向兼容性保证,请在 llvm-dev 邮件列表中提出问题。
LLVM 仍然支持使用 gcroot
intrinsic 的保守垃圾回收支持的替代机制。gcroot
机制在很大程度上是出于历史兴趣,但有一个例外 - 其影子栈的实现已被许多语言前端成功使用,并且仍然受支持。
概述 & 核心概念¶
为了回收死对象,垃圾回收器必须能够识别执行代码中包含的对对象的任何引用,并且根据收集器的不同,可能需要更新它们。收集器不需要在代码的所有点都获得此信息 - 这将使问题变得更加困难 - 而只需要在执行中称为“安全点”的明确定义的点上。对于大多数收集器来说,跟踪每个唯一指针值的至少一个副本就足够了。但是,对于希望重定位可从运行代码直接访问的对象的收集器来说,需要更高的标准。
另一个额外的挑战是,编译器可能会计算指向分配外部甚至另一个分配中间的中间结果(“派生指针”)。此中间值的最终使用必须产生分配边界内的地址,但这种“外部派生指针”可能对收集器可见。鉴于此,垃圾回收器不能安全地依赖地址的运行时值来指示与其关联的对象。如果垃圾回收器希望移动任何对象,则编译器必须为每个指针提供一个映射,以指示其分配。
为了简化收集器和编译代码之间的交互,大多数垃圾回收器都以三个抽象概念组织:加载屏障、存储屏障和安全点。
加载屏障是在机器加载指令之后立即执行的一段代码,但在使用加载的值之前执行。根据收集器的不同,所有加载、仅特定类型(在原始源语言中)的加载或根本不需要屏障。
类似地,存储屏障是在机器存储指令之前立即运行的代码片段,但在计算存储值之后运行。存储屏障最常见的用途是更新分代垃圾回收器中的“卡片表”。
安全点是编译代码可见的指针(即当前在寄存器或堆栈中)被允许更改的位置。在安全点完成后,实际指针值可能会有所不同,但指向的“对象”(如源语言所见)将不会改变。
请注意,“安全点”一词有些过载。它既指机器状态可解析的位置,也指将应用程序线程带到收集器可以安全使用该信息的点的协调协议。本文档中使用的术语“状态点”仅指前者。
本文档重点介绍最后一项 - 编译器对生成代码中安全点的支持。我们将假设外部机制已决定在何处放置安全点。从我们的角度来看,所有安全点都将是函数调用。为了支持重定位可从编译代码中的值直接访问的对象,收集器必须能够
识别安全点处指针的每个副本(包括编译器本身引入的副本),
识别每个指针与哪个对象相关,以及
可能更新每个副本。
本文档描述了基于 LLVM 的编译器可以向语言运行时/收集器提供此信息,并确保可以读取和更新所有指针(如果需要)的机制。
抽象机器模型¶
在高层次上,LLVM 已被扩展以支持编译到抽象机器,该抽象机器扩展了实际目标,并具有非整型指针类型,适用于表示对对象的垃圾回收引用。特别是,这种非整型指针类型没有定义到整数表示的映射。这种语义上的怪癖允许运行时为程序中的每个点选择一个整数映射,从而允许重定位对象而没有可见效果。
这种高层次的抽象机器模型用于大多数优化器。因此,转换 Pass 不需要扩展以查看显式重定位序列。在开始代码生成之前,我们将表示切换为显式形式。选择降低的确切位置是实现细节。
请注意,抽象机器模型的大部分价值来自需要建模潜在可重定位对象的收集器。对于仅支持非重定位收集器的编译器,您可能希望考虑从完全显式形式开始。
警告:在非整型指针的定义中,目前已知存在一个语义漏洞,尚未在上游解决。为了解决这个问题,您需要禁用加载的推测,除非内存类型(非整型指针与任何其他类型)已知为未更改。也就是说,如果推测加载导致非整型指针值被加载为任何其他类型或反之亦然,则推测加载是不安全的。在实践中,此限制很好地隔离在 ValueTracking.cpp 中的 isSafeToSpeculate 中。
显式表示¶
前端可以直接生成这种低级显式形式,但这样做可能会抑制优化。相反,建议使用重定位收集器的编译器以刚刚描述的抽象机器模型为目标。
显式方法的核心是以一种方式构造(或重写)IR,使垃圾回收器执行的可能更新在 IR 中显式可见。这样做需要我们
为每个可能重定位的指针创建一个新的 SSA 值,并确保在安全点之后无法访问原始(非重定位)值的任何用途,
以对编译器不透明的方式指定重定位,以确保优化器在状态点之后无法引入未重定位值的新用途。这可以防止优化器执行不健全的优化。
记录每个状态点的活动指针(及其关联的分配)的映射。
在最抽象的层面上,插入安全点可以被认为是将调用指令替换为对多返回值函数的调用,该函数既调用原始调用的目标,又返回其结果,并返回垃圾回收对象的任何活动指针的更新值。
请注意,识别指向垃圾回收值的所有活动指针、转换 IR 以公开为每个此类活动指针提供基本对象的指针以及正确插入所有 intrinsic 的任务明确超出了本文档的范围。推荐的方法是使用下面描述的 实用工具 Pass。
这个抽象函数调用具体地由一系列称为“状态点重定位序列”的 intrinsic 调用表示。
让我们考虑 LLVM IR 中的一个简单调用
declare void @foo()
define ptr addrspace(1) @test1(ptr addrspace(1) %obj)
gc "statepoint-example" {
call void @foo()
ret ptr addrspace(1) %obj
}
根据我们的语言,我们可能需要在执行 foo
期间允许安全点。如果是这样,我们需要让收集器更新当前帧中的局部值。如果我们不这样做,一旦我们最终从调用返回,我们将访问潜在的无效引用。
在这个例子中,我们需要重定位 SSA 值 %obj
。由于我们实际上无法更改 SSA 值 %obj
中的值,我们需要引入一个新的 SSA 值 %obj.relocated
,它表示 %obj
在安全点之后可能已更改的值,并适当地更新任何后续用途。结果重定位序列是
define ptr addrspace(1) @test(ptr addrspace(1) %obj)
gc "statepoint-example" {
%safepoint = call token (i64, i32, ptr, i32, i32, ...) @llvm.experimental.gc.statepoint.p0f_isVoidf(i64 0, i32 0, ptr elementtype(void ()) @foo, i32 0, i32 0, i32 0, i32 0) ["gc-live" (ptr addrspace(1) %obj)]
%obj.relocated = call ptr addrspace(1) @llvm.experimental.gc.relocate.p1(token %safepoint, i32 0, i32 0)
ret ptr addrspace(1) %obj.relocated
}
理想情况下,此序列应表示为 M 参数、N 返回值函数(其中 M 是要重定位的值的数量 + 原始调用参数,N 是原始返回值 + 每个重定位的值),但 LLVM 不容易支持这种表示。
相反,statepoint intrinsic 标记了安全点或状态点的实际位置。状态点返回一个 token 值(仅在编译时存在)。为了取回调用的原始返回值,我们使用 gc.result
intrinsic。为了依次获取每个指针的重定位,我们使用具有适当索引的 gc.relocate
intrinsic。请注意,gc.relocate
和 gc.result
都绑定到状态点。该组合形成一个“状态点重定位序列”,并表示可解析调用或“状态点”的全部。
降低后,此示例将生成以下 x86 汇编代码
.globl test1
.align 16, 0x90
pushq %rax
callq foo
.Ltmp1:
movq (%rsp), %rax # This load is redundant (oops!)
popq %rdx
retq
每个可能重定位的值都已溢出到堆栈,并且该位置的记录已记录到 Stack Map section。如果垃圾回收器需要在调用期间更新任何这些指针,它确切地知道要更改什么。
我们的示例的 StackMap section 的相关部分是
# This describes the call site
# Stack Maps: callsite 2882400000
.quad 2882400000
.long .Ltmp1-test1
.short 0
# .. 8 entries skipped ..
# This entry describes the spill slot which is directly addressable
# off RSP with offset 0. Given the value was spilled with a pushq,
# that makes sense.
# Stack Maps: Loc 8: Direct RSP [encoding: .byte 2, .byte 8, .short 7, .int 0]
.byte 2
.byte 8
.short 7
.long 0
此示例取自 RewriteStatepointsForGC 实用工具 Pass 的测试。因此,可以使用以下命令轻松检查其完整的 StackMap。
opt -rewrite-statepoints-for-gc test/Transforms/RewriteStatepointsForGC/basics.ll -S | llc -debug-only=stackmaps
非重定位 GC 的简化¶
对于非重定位收集器来说,前一个示例中的某些复杂性是不必要的。虽然非重定位收集器仍然需要有关哪些位置包含活动引用的信息,但它不需要表示显式重定位。因此,先前描述的显式降低可以简化为删除所有 gc.relocate
intrinsic 调用,并将用途保留在原始引用值方面。
这是非重定位收集器先前示例的显式降低
define void @manual_frame(ptr %a, ptr %b) gc "statepoint-example" {
%alloca = alloca ptr
%allocb = alloca ptr
store ptr %a, ptr %alloca
store ptr %b, ptr %allocb
call token (i64, i32, ptr, i32, i32, ...) @llvm.experimental.gc.statepoint.p0(i64 0, i32 0, ptr elementtype(void ()) @func, i32 0, i32 0, i32 0, i32 0) ["gc-live" (ptr %alloca, ptr %allocb)]
ret void
}
记录栈上区域¶
除了先前描述的显式重定位形式外,状态点基础设施还允许列出 gc 指针列表中的 alloca。可以列出带有或不带有附加显式 gc 指针值和重定位的 Alloca。
状态点操作数列表的 gc 区域中的 alloca 将导致堆栈区域的地址列在状态点的 stackmap 中。
如果需要,此机制可用于描述显式溢出槽。然后,生成器有责任确保在安全点的任一侧根据需要将值溢出/填充到/从 alloca。请注意,没有办法指示此类显式指定的溢出槽的相应基指针,因此使用仅限于相关收集器可以从指针本身派生对象基址的值。
此机制可用于描述包含引用的栈上对象,前提是收集器可以将栈上的位置映射到描述收集器需要处理的引用的内部布局的堆映射。
警告:目前,这种替代形式没有得到很好的实践。建议谨慎使用它,并期望必须修复一些 bug。特别是,RewriteStatepointsForGC 实用工具 Pass 今天不对 alloca 执行任何操作。
基指针 & 派生指针¶
“基指针”是指向分配(对象)起始地址的指针。“派生指针”是指相对于基指针偏移一定量的指针。在重定位对象时,垃圾回收器需要能够将与分配关联的每个派生指针重定位到距新地址相同的偏移量。
“内部派生指针”保留在与其关联的分配的边界内。因此,如果运行时系统知道分配的边界,则可以在运行时找到基本对象。
“外部派生指针”在关联对象的边界之外;它们甚至可能落在另一个分配地址范围内。因此,垃圾回收器无法在运行时确定它们与哪个分配关联,因此需要编译器支持。
gc.relocate
intrinsic 支持显式操作数,用于描述与派生指针关联的分配。此操作数通常称为基本操作数,但严格来说不一定是基指针,但确实需要位于关联分配的边界内。某些收集器可能要求操作数是实际的基指针,而不仅仅是内部派生指针。请注意,在降低期间,即使基指针在之后未使用,也要求基指针和派生指针操作数在关联的调用安全点上都处于活动状态。
GC 转换¶
作为一个实际的考虑因素,许多垃圾回收系统允许收集器感知代码(“托管代码”)调用不收集器感知代码(“非托管代码”)。通常,此类调用也必须是安全点,因为希望允许收集器在执行非托管代码期间运行。此外,通常协调从托管代码到非托管代码的转换需要在调用站点生成额外的代码,以通知收集器转换。为了支持这些需求,状态点可以标记为 GC 转换,并且执行转换所需的数据(如果有)可以作为状态点的附加参数提供。
请注意,虽然在许多情况下,可以根据所涉及的函数符号推断状态点为 GC 转换(例如,从具有 GC 策略“foo”的函数到具有 GC 策略“bar”的函数的调用),但也必须支持也是 GC 转换的间接调用。此要求是需要显式标记 GC 转换的决策的驱动力。
让我们重新审视上面给出的示例,这次将对 @foo
的调用视为 GC 转换。根据我们的目标,转换代码可能需要访问一些额外的状态,以便通知收集器转换。让我们假设一个假设的 GC - 有些缺乏想象力地命名为“hypothetical-gc” - 它要求在调用非托管代码之前和之后写入 TLS 变量。结果重定位序列是
@flag = thread_local global i32 0, align 4
define i8 addrspace(1)* @test1(i8 addrspace(1) *%obj)
gc "hypothetical-gc" {
%0 = call token (i64, i32, void ()*, i32, i32, ...)* @llvm.experimental.gc.statepoint.p0f_isVoidf(i64 0, i32 0, void ()* @foo, i32 0, i32 1, i32* @Flag, i32 0, i8 addrspace(1)* %obj)
%obj.relocated = call coldcc i8 addrspace(1)* @llvm.experimental.gc.relocate.p1i8(token %0, i32 7, i32 7)
ret i8 addrspace(1)* %obj.relocated
}
在降低期间,这将导致指令选择 DAG,如下所示
CALLSEQ_START
...
GC_TRANSITION_START (lowered i32 *@Flag), SRCVALUE i32* Flag
STATEPOINT
GC_TRANSITION_END (lowered i32 *@Flag), SRCVALUE i32 *Flag
...
CALLSEQ_END
为了生成必要的转换代码,必须修改“hypothetical-gc”支持的每个目标的后端,以便在使用特定功能的“hypothetical-gc”策略时适当地降低 GC_TRANSITION_START
和 GC_TRANSITION_END
节点。假设已为 X86 添加了此类降低,则生成的汇编代码将是
.globl test1
.align 16, 0x90
pushq %rax
movl $1, %fs:Flag@TPOFF
callq foo
movl $0, %fs:Flag@TPOFF
.Ltmp1:
movq (%rsp), %rax # This load is redundant (oops!)
popq %rdx
retq
请注意,上面呈现的设计尚未完全实现:特别是,特定于策略的降低不存在,并且所有 GC 转换都作为调用指令之前和之后的单个 no-op 发出。这些 no-op 通常在死机器指令消除期间被后端删除。
在抽象机器模型通过 RewriteStatepointsForGC Pass 降低到显式状态点重定位模型之前,任何派生指针都可以通过分别使用 gc.get.pointer.base
和 gc.get.pointer.offset
intrinsic 来获取其基指针和相对于基指针的偏移量。这些 intrinsic 由 RewriteStatepointsForGC Pass 内联,并且在此 Pass 之后不得使用。
栈映射格式¶
运行时或收集器可能需要读取和/或更新的每个指针值的位置都在生成的对象文件的单独部分中提供,如 PatchPoint 文档中所述。此特殊部分按照 Stack Map format 进行编码。
一般的预期是 JIT 编译器将解析并丢弃此格式;它不是特别节省内存。如果您需要替代格式(例如,对于提前编译编译器),请参阅 :ref: 未完成的工作项 <OpenWork> 下的讨论。
每个状态点生成以下位置
描述调用目标的调用约定的常量。此常量是用于生成 stackmap 的 LLVM 版本的有效 调用约定标识符。对于此常量,除了 LLVM 在其他地方提供的关于这些标识符的内容外,不提供额外的兼容性保证。
描述传递给状态点 intrinsic 的标志的常量
描述以下 deopt 位置(不是操作数)数量的常量。如果未提供“deopt” bundle,则将为 0。
可变数量的位置,每个位置对应于“deopt”操作数 bundle 中列出的每个 deopt 参数。目前,仅支持位宽为 64 位或更小的 deopt 参数。只有在以下情况下,才能指定和报告大于 64 位类型的值:a) 该值在调用站点是常量,并且 b) 假设零扩展到原始位宽,该常量可以用小于 64 位表示。
可变数量的重定位记录,每个记录正好由两个位置组成。重定位记录在下面详细描述。
每个重定位记录都为收集器提供了足够的信息来重定位一个或多个派生指针。每个记录都由一对位置组成。记录中的第二个元素表示需要更新的指针。记录中的第一个元素提供指向与正在重定位的指针关联的对象的基址的指针。处理广义派生指针需要此信息,因为指针可能在原始分配的边界之外,但仍然需要随分配一起重定位。此外
保证如果基指针在状态点之后使用,则基指针也必须显式地作为重定位对出现。
重定位记录的数量可能少于 IR 状态点中的 gc 参数。每个唯一对将至少出现一次;重复是可能的。
每个记录中的位置可以是指针大小或指针大小的倍数。在后一种情况下,记录必须解释为描述一系列指针及其对应的基指针。如果位置的大小为 N x sizeof(pointer),则将有 N 个每个包含一个指针的记录包含在该位置内。可以假定一对中的两个位置的大小相同。
请注意,每个部分中使用的位置可能描述相同的物理位置。例如,堆栈槽可能显示为 deopt 位置、gc 基指针和 gc 派生指针。
对于状态点记录,StkMapRecord 的 LiveOut 部分将为空。
安全点语义 & 验证¶
对于编译代码的正确性相对于垃圾回收器,基本的正确性属性是动态的。必须是这种情况,即不存在动态跟踪,使得涉及可能重定位的指针的操作在可能重定位它的安全点之后是可观察的。“可观察之后”在此用法中意味着外部观察者可以以某种方式观察到此事件序列,从而排除在安全点之前执行的操作。
为了理解为什么需要此“可观察之后”属性,请考虑对重定位指针的原始副本执行的空比较。假设控制流跟随安全点,则无法从外部观察到空比较是在安全点之前还是之后执行的。(请记住,原始值未被安全点修改。)编译器可以自由地做出任一调度选择。
实现的实际正确性属性比这稍强。我们要求在静态路径上,可能重定位的指针在其可能已被重定位之后不是“可观察的”。这比严格意义上必要的要稍微强一些(因此可能会禁止某些其他有效的程序),但大大简化了对编译代码正确性的推理。
通过构造,如果源 IR 中正确建立此属性,则优化器将支持此属性。这是设计的关键不变量。
现有的 IR Verifier Pass 已扩展为检查其各自文档中提到的大多数关于 intrinsic 的局部限制。LLVM 中的当前实现不检查关键的重定位不变量,但这正在进行开发此类验证器的工作。如果您有兴趣尝试当前版本,请在 llvm-dev 上提问。
用于安全点插入的实用工具 Pass¶
RewriteStatepointsForGC¶
Pass RewriteStatepointsForGC 转换函数的 IR,以从上面描述的抽象机器模型降低到显式状态点重定位模型。为此,它将可能包含安全点轮询的函数的所有调用或调用替换为 gc.statepoint
和关联的完整重定位序列,包括所有必需的 gc.relocates
。
此 Pass 仅适用于设置了 UseRS4GC
标志的 GCStrategy 实例。设置此标志的两个内置 GC 策略是“statepoint-example”和“coreclr”策略。
例如,给定以下代码
define ptr addrspace(1) @test1(ptr addrspace(1) %obj)
gc "statepoint-example" {
call void @foo()
ret ptr addrspace(1) %obj
}
Pass 将生成此 IR
define ptr addrspace(1) @test_rs4gc(ptr addrspace(1) %obj) gc "statepoint-example" {
%statepoint_token = call token (i64, i32, ptr, i32, i32, ...) @llvm.experimental.gc.statepoint.p0(i64 2882400000, i32 0, ptr elementtype(void ()) @foo, i32 0, i32 0, i32 0, i32 0) [ "gc-live"(ptr addrspace(1) %obj) ]
%obj.relocated = call coldcc ptr addrspace(1) @llvm.experimental.gc.relocate.p1(token %statepoint_token, i32 0, i32 0) ; (%obj, %obj)
ret ptr addrspace(1) %obj.relocated
}
在上面的示例中,指针上的 addrspace(1) 标记是 statepoint-example
GC 策略用于区分引用和非引用的机制。这通过 GCStrategy::isGCManagedPointer 控制。statepoint-example
和 coreclr
策略(仅有的两个默认策略支持状态点)都使用 addrspace(1) 来确定哪些指针是引用,但是自定义策略不必遵循此约定。
语言前端可以使用此 Pass 作为实用工具函数,该前端不想在构造 IR 时手动推理活跃度、基指针或重定位。按照目前的实现,RewriteStatepointsForGC 必须在 SSA 构造(即 mem2ref)之后运行。
RewriteStatepointsForGC 将确保为创建的每个重定位列出适当的基指针。它将通过根据需要复制代码来做到这一点,以将与每个要重定位的指针关联的基指针传播到适当的安全点。该实现假定以下 IR 构造生成基指针:从堆加载、全局变量的地址、函数参数、函数返回值。常量指针(例如 null)也被假定为基指针。在实践中,如果目标收集器可以从任意内部派生指针找到关联的分配,则可以将此约束放宽为生成内部派生指针。
默认情况下,RewriteStatepointsForGC 传入 0xABCDEF00
作为状态点 ID,0
作为新构造的 gc.statepoint
的可修补字节数。这些值可以在每个调用站点上使用属性 "statepoint-id"
和 "statepoint-num-patch-bytes"
进行配置。如果调用站点标记有 "statepoint-id"
函数属性,并且其值为正整数(表示为字符串),则该值用作新构造的 gc.statepoint
的 ID。如果调用站点标记有 "statepoint-num-patch-bytes"
函数属性,并且其值为正整数,则该值用作新构造的 gc.statepoint
的“num patch bytes”参数。如果可以成功解析 "statepoint-id"
和 "statepoint-num-patch-bytes"
属性,则它们不会传播到 gc.statepoint
调用或调用。
在实践中,RewriteStatepointsForGC 应在 Pass 管线中稍后运行,在大多数优化已经完成之后。这有助于提高在使用垃圾回收支持编译时生成的代码的质量。
RewriteStatepointsForGC intrinsic 降低¶
作为降低到显式重定位模型的一部分,RewriteStatepointsForGC 对以下 intrinsic 执行 GC 特定降低
gc.get.pointer.base
gc.get.pointer.offset
llvm.memcpy.element.unordered.atomic.*
llvm.memmove.element.unordered.atomic.*
memcpy 和 memmove 操作有两种可能的 lowering 方式:GC 叶节点 lowering 和 GC 可解析 lowering。如果一个调用被显式标记了 “gc-leaf-function” 属性,则该调用将 lowering 为对 ‘__llvm_memcpy_element_unordered_atomic_*
’ 或 ‘__llvm_memmove_element_unordered_atomic_*
’ 符号的 GC 叶节点调用。这种调用不能获取安全点。否则,通过将调用包装到状态点中,使调用变为 GC 可解析的。这使得在复制操作期间可以获取安全点。请注意,GC 可解析的复制操作并非必须获取安全点。例如,短复制操作可能在不获取安全点的情况下执行。
对 ‘llvm.memcpy.element.unordered.atomic.*
’、‘llvm.memmove.element.unordered.atomic.*
’ intrinsic 的 GC 可解析调用分别 lowering 为对 ‘__llvm_memcpy_element_unordered_atomic_safepoint_*
’、‘__llvm_memmove_element_unordered_atomic_safepoint_*
’ 符号的调用。这样,运行时可以提供带有和不带安全点的复制操作的实现。
GC 可解析 lowering 还涉及到调整调用的参数。Memcpy 和 memmove intrinsic 接受派生指针作为源和目标参数。如果复制操作获取安全点,则可能需要重定位底层的源对象和目标对象。这要求相应的基指针在复制操作中可用。为了使基指针可用,RewriteStatepointsForGC 将派生指针替换为基指针和偏移量对。例如
declare void @__llvm_memcpy_element_unordered_atomic_safepoint_1(
i8 addrspace(1)* %dest_base, i64 %dest_offset,
i8 addrspace(1)* %src_base, i64 %src_offset,
i64 %length)
PlaceSafepoints¶
PlaceSafepoints pass 插入足够的安全点轮询,以确保运行代码及时检查安全点请求。此 pass 预计在 RewriteStatepointsForGC 之前运行,因此不会生成完整的重定位序列。
例如,给定以下输入 IR
define void @test() gc "statepoint-example" {
call void @foo()
ret void
}
declare void @do_safepoint()
define void @gc.safepoint_poll() {
call void @do_safepoint()
ret void
}
此 pass 将生成以下 IR
define void @test() gc "statepoint-example" {
call void @do_safepoint()
call void @foo()
ret void
}
在这种情况下,我们添加了一个(无条件)入口安全点轮询。请注意,尽管看起来如此,但入口轮询不一定是冗余的。我们需要知道 foo
和 test
不是相互递归的,轮询才是冗余的。在实践中,您可能希望您的轮询定义包含某种形式的条件分支。
目前,PlaceSafepoints 可以在方法入口和循环回边位置插入安全点轮询。如果需要,扩展它以支持返回轮询将很简单。
PlaceSafepoints 包含许多优化,以避免在特定站点放置安全点轮询,除非需要确保在正常条件下及时执行轮询。PlaceSafepoints 不尝试确保在最坏情况下(例如严重的系统分页)及时执行轮询。
安全点轮询动作的实现通过在包含的模块中查找名为 gc.safepoint_poll
的函数来指定。此函数的主体插入到每个所需的轮询站点。虽然在此方法内部的调用或调用指令被转换为 gc.statepoints
,但不会执行递归轮询插入。
对于任何只需要在安全点支持垃圾回收语义的语言前端,此 pass 都很有用。如果您需要在安全点获取其他抽象帧信息(例如,用于反优化或内省),您可以在前端插入安全点轮询。如果您有后一种情况,请在 llvm-dev 上寻求建议。在使这种方案在实践中良好运行方面已经做了很多工作,但尚未在此处记录。
支持的架构¶
对 statepoint 生成的支持需要每个后端的一些代码。目前,仅支持 Aarch64 和 X86_64。
局限性和半生不熟的想法¶
混合引用和原始指针¶
在抽象机模型中,支持允许非托管指针指向垃圾回收对象的语言(即,将指向对象的指针传递给 C 例程)。目前,关于如何处理这个问题的最佳想法涉及 intrinsic 或 opaque 函数,它们隐藏了引用值和原始指针之间的连接。问题在于,ptrtoint 或 inttoptr 转换(在这种用例中很常见)破坏了用于在从抽象模型 lowering 到显式物理模型时推断任意引用的基指针的规则。请注意,直接 lowering 到物理模型的前端在这里没有任何问题。
栈上的对象¶
如上所述,显式 lowering 支持在栈上分配的对象,前提是垃圾回收器可以根据栈地址找到堆映射。
缺失的部分是 a) 从抽象机模型进行重写 (RS4GC) 的集成,以及 b) 支持可选地分解栈上的对象,从而不需要它们的堆映射。后者是与某些垃圾回收器轻松集成所必需的。
Lowering 质量和表示开销¶
已知当前 statepoint lowering 质量有些差。从长远来看,我们希望将 statepoint 与寄存器分配器集成;在短期内,这不太可能发生。我们发现 lowering 的质量相对而言并不重要,因为热点 statepoint 几乎总是内联错误。
有人担心 statepoint 表示会导致为某些示例生成大量 IR,并且这会导致高于预期的内存使用量和编译时间。目前没有计划因此进行更改,但未来可能会探索替代模型。
沿异常边的重定位¶
目前,沿异常路径的重定位在 ToT 中已损坏。特别是,目前无法表示在也具有重定位的路径上的 rethrow。有关更多详细信息,请参阅 此 llvm-dev 讨论。
Bug 和增强¶
当前已知的 bug 和正在考虑的增强可以通过在摘要字段中执行 bugzilla 搜索 [Statepoint] 来跟踪。在提交新 bug 时,请使用此标签,以便感兴趣的各方看到新提交的 bug。与大多数 LLVM 功能一样,设计讨论在 Discourse 论坛 上进行,补丁应发送到 llvm-commits 进行审查。