LLVM 中的垃圾回收安全点

状态

本文档描述了一组对 LLVM 的扩展,以支持垃圾回收。到目前为止,这些机制已通过商业 Java 实现得到充分验证,并且已使用完全重新定位的收集器进行发货。在一些地方可能仍然存在错误;这些在下面列出。

它们仍被列为“实验性”,以表明在不同版本之间不提供向前或向后兼容性保证。如果您的用例需要某种形式的向前兼容性保证,请在 llvm-dev 邮件列表中提出问题。

LLVM 仍然支持使用 gcroot 本质的保守垃圾回收支持的替代机制。gcroot 机制在这一点上主要具有历史意义,但有一个例外——其影子栈的实现已成功地被许多语言前端使用,并且仍然受支持。

概述和核心概念

为了收集死对象,垃圾收集器必须能够识别执行代码中包含的对象的任何引用,并且,根据收集器的不同,可能需要更新它们。收集器并非在代码的所有点都需要此信息——这会使问题变得更加困难——而只需要在执行中的明确定义的点,称为“安全点”。对于大多数收集器来说,跟踪每个唯一指针值的至少一个副本就足够了。但是,对于希望重新定位从运行代码直接可达的对象的收集器,则需要更高的标准。

另一个挑战是编译器可能会计算指向分配外部甚至指向另一个分配中间的中间结果(“派生指针”)。此中间值的最终使用必须产生分配边界内的地址,但此类“外部派生指针”可能对收集器可见。鉴于此,垃圾收集器不能安全地依赖地址的运行时值来指示其关联的对象。如果垃圾收集器希望移动任何对象,则编译器必须为每个指针提供一个映射,以指示其分配。

为了简化收集器和编译代码之间的交互,大多数垃圾收集器都围绕三个抽象进行组织:加载屏障、存储屏障和安全点。

  1. 加载屏障是在机器加载指令之后立即执行的一段代码,但在加载值的任何使用之前。根据收集器的不同,所有加载、特定类型(在原始源语言中)的加载或根本不需要加载都需要此类屏障。

  2. 类似地,存储屏障是在机器存储指令之前运行的代码片段,但在存储值的计算之后。存储屏障最常见的用途是在分代垃圾收集器中更新“卡表”。

  3. 安全点是允许编译代码可见的指针(即当前在寄存器或栈中)更改的位置。在安全点完成后,实际的指针值可能会有所不同,但指向的“对象”(如源语言所见)不会改变。

请注意,“安全点”一词有点过载。它既指机器状态可解析的位置,也指将应用程序线程带到收集器可以安全地使用该信息的位置的协调协议。“状态点”在本文件中仅指前者。

本文档重点介绍最后一项——生成代码中安全点的编译器支持。我们将假设外部机制已决定在哪里放置安全点。从我们的角度来看,所有安全点都将是函数调用。为了支持对从编译代码中的值直接可达的对象进行重新定位,收集器必须能够

  1. 识别安全点处每个指针的副本(包括编译器本身引入的副本),

  2. 识别每个指针与哪个对象相关,以及

  3. 可能更新这些副本中的每一个。

本文档描述了基于 LLVM 的编译器如何向语言运行时/收集器提供此信息,并确保可以在需要时读取和更新所有指针的机制。

抽象机器模型

在高级别上,LLVM 已扩展为支持编译到扩展实际目标的抽象机器,该机器具有适合表示对对象的垃圾回收引用的非整型指针类型。特别是,此类非整型指针类型没有定义到整型表示的映射。此语义特性允许运行时为程序中的每个点选择一个整型映射,从而允许对象重新定位而没有明显的影响。

此高级抽象机器模型用于大多数优化器。因此,转换传递不需要扩展以查看显式重新定位序列。在开始代码生成之前,我们将表示切换到显式形式。选择的降低确切位置是实现细节。

请注意,抽象机器模型的大部分价值来自需要对可能重新定位的对象进行建模的收集器。对于仅支持非重新定位收集器的编译器,您可能希望考虑从完全显式形式开始。

警告:非整型指针定义中目前存在一个已知的语义漏洞,尚未在上游解决。为了解决此问题,您需要禁用加载的推测,除非内存类型(非整型指针与任何其他类型)已知不变。也就是说,如果这样做会导致非整型指针值加载为任何其他类型或反之亦然,则推测加载是不安全的。在实践中,此限制很好地隔离到 ValueTracking.cpp 中的 isSafeToSpeculate。

显式表示

前端可以直接生成此低级显式形式,但这样做可能会抑制优化。相反,建议具有重新定位收集器的编译器以刚刚描述的抽象机器模型为目标。

显式方法的核心是以一种方式构造(或重写)IR,在这种方式下,垃圾收集器执行的可能更新在 IR 中显式可见。这样做要求我们

  1. 为每个可能重新定位的指针创建一个新的 SSA 值,并确保在安全点之后无法访问原始(非重新定位)值的任何使用,

  2. 以对编译器不透明的方式指定重新定位,以确保优化器无法在状态点之后引入未重新定位值的新的使用。这可以防止优化器执行不安全的优化。

  3. 记录每个状态点活动指针(及其关联的分配)的映射。

在最抽象的层面上,插入安全点可以被认为是用调用原始目标的调用以及返回其结果并返回任何活动指针对垃圾收集对象的更新值的多个返回值函数的调用来替换调用指令。

请注意,识别所有活动指针对垃圾收集值、转换 IR 以公开每个此类活动指针的基础对象的指针以及正确插入所有本质的任务明确不在本文档的范围内。建议的方法是使用下面描述的实用程序传递

此抽象函数调用由一组称为“状态点重新定位序列”的本质调用具体表示。

让我们考虑 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 不容易支持此类表示。

相反,状态点本质标记了安全点或状态点的实际位置。状态点返回一个令牌值(仅在编译时存在)。要获取调用的原始返回值,我们使用 gc.result 本质。要依次获取每个指针的重新定位,我们使用 gc.relocate 本质以及适当的索引。请注意,gc.relocategc.result 都与状态点相关联。组合形成“状态点重新定位序列”,并表示可解析调用或“状态点”的全部内容。

降低后,此示例将生成以下 x86 汇编

        .globl        test1
        .align        16, 0x90
        pushq %rax
        callq foo
.Ltmp1:
        movq  (%rsp), %rax  # This load is redundant (oops!)
        popq  %rdx
        retq

每个可能重新定位的值都已溢出到栈中,并且该位置的记录已记录到栈映射部分。如果垃圾收集器需要在调用期间更新这些指针中的任何一个,它就知道该更改什么。

我们示例的栈映射部分的相关部分为

# 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实用程序传递的测试。因此,可以使用以下命令轻松检查其完整的 StackMap。

opt -rewrite-statepoints-for-gc test/Transforms/RewriteStatepointsForGC/basics.ll -S | llc -debug-only=stackmaps

非重新定位 GC 的简化

先前示例中的一些复杂性对于非重新定位收集器来说是不必要的。虽然非重新定位收集器仍然需要有关哪些位置包含活动引用的信息,但它不需要表示显式重新定位。因此,前面描述的显式降低可以简化为删除所有gc.relocate内在函数调用,并保留原始引用值的用法。

以下是针对非重新定位收集器的先前示例的显式降低。

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。Alloca 可以带或不带其他显式 gc 指针值和重新定位进行列出。

状态点操作数列表的 gc 区域中的 alloca 将导致栈区域的地址在状态点的栈映射中列出。

此机制可用于根据需要描述显式溢出槽。然后,它成为生成器负责确保在安全点两侧根据需要将值溢出/填充到/从 alloca 中。请注意,无法指示此类显式指定的溢出槽的相应基指针,因此使用仅限于收集器可以从指针本身推导出对象基的值。

此机制可用于描述包含引用的栈上对象,前提是收集器可以从栈上的位置映射到描述收集器需要处理的引用的内部布局的堆映射。

警告:目前,这种替代形式没有得到很好的使用。建议谨慎使用,并预计需要修复一些错误。特别是,RewriteStatepointsForGC 实用程序传递目前对 alloca 没有任何作用。

基指针和派生指针

“基指针”是指向分配(对象)起始地址的指针。“派生指针”是从基指针偏移一定量的指针。在重新定位对象时,垃圾收集器需要能够将与分配关联的每个派生指针重新定位到新地址的相同偏移量。

“内部派生指针”保持在其关联的分配的范围内。因此,如果运行时系统知道分配的范围,则可以在运行时找到基对象。

“外部派生指针”位于关联对象的范围之外;它们甚至可能位于*另一个*分配的地址范围内。因此,垃圾收集器无法在运行时确定它们与哪个分配相关联,并且需要编译器支持。

gc.relocate内在函数支持一个用于描述与派生指针关联的分配的显式操作数。此操作数通常称为基操作数,但严格来说不必是基指针,但它确实需要位于关联分配的范围内。一些收集器可能要求操作数是实际的基指针,而不仅仅是内部派生指针。请注意,在降低过程中,即使基指针之后不再使用,也需要基指针和派生指针操作数在关联的调用安全点上保持活动状态。

GC 转换

作为一项实际考虑,许多垃圾收集系统允许了解收集器的代码(“托管代码”)调用不了解收集器的代码(“非托管代码”)。通常,此类调用也必须是安全点,因为希望允许收集器在非托管代码执行期间运行。此外,协调从托管代码到非托管代码的转换通常需要在调用站点生成额外的代码来通知收集器转换。为了支持这些需求,状态点可以标记为 GC 转换,并且执行转换(如果有)所需的数据可以作为附加参数提供给状态点。

请注意,尽管在许多情况下,可以根据所涉及的函数符号(例如,从具有 GC 策略“foo”的函数到具有 GC 策略“bar”的函数的调用)推断状态点是否为 GC 转换,但还必须支持作为 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_STARTGC_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 转换都作为调用指令之前和之后的单个空操作发出。这些空操作通常在后端执行死机器指令消除时被删除。

在抽象机器模型由RewriteStatepointsForGC传递降低到重新定位的显式状态点模型之前,任何派生指针都可以通过使用gc.get.pointer.basegc.get.pointer.offset内在函数分别获取其基指针和从基指针的偏移量。这些内在函数由RewriteStatepointsForGC传递内联,并且在此传递后不得使用。

栈映射格式

可能需要运行时或收集器读取和/或更新的每个指针值的地址都以单独的部分形式提供在生成的物体文件中,如 PatchPoint 文档中所述。此特殊部分根据栈映射格式进行编码。

通常期望 JIT 编译器将解析并丢弃此格式;它在内存效率方面不是很高。如果您需要其他格式(例如,用于提前编译器),请参阅下面 :ref: 开放工作项目 <OpenWork> 下的讨论。

每个状态点生成以下位置

  • 描述调用目标调用约定的常量。此常量是用于生成栈映射的 LLVM 版本的有效调用约定标识符。对于此常量,除了 LLVM 在其他地方提供的关于这些标识符的内容外,不提供其他兼容性保证。

  • 描述传递给状态点内在函数的标志的常量。

  • 描述以下 deopt 位置(而不是操作数)数量的常量。如果没有提供“deopt”包,则为 0。

  • 可变数量的位置,每个“deopt”操作数包中列出的每个 deopt 参数一个。目前,仅支持位宽为 64 位或更小的 deopt 参数。只有在 a) 值在调用站点是常量,并且 b) 常量可以用小于 64 位表示(假设零扩展到原始位宽)时,才能指定和报告大于 64 位类型的值。

  • 可变数量的重新定位记录,每个记录都包含两个位置。重新定位记录将在下面详细描述。

每个重新定位记录都提供足够的信息供收集器重新定位一个或多个派生指针。每个记录都由一对位置组成。记录中的第二个元素表示需要更新的指针(或指针)。记录中的第一个元素提供指向与要重新定位的指针关联的对象的基地址的指针。此信息对于处理广义派生指针是必需的,因为指针可能位于原始分配的范围之外,但仍需要与分配一起重新定位。此外

  • 保证如果在状态点之后使用基指针,则基指针也必须作为重新定位对显式出现。

  • 重新定位记录的数量可能少于 IR 状态点中的 gc 参数。每个唯一对将至少出现一次;重复是可能的。

  • 每个记录中的位置可以是指针大小或指针大小的倍数。在后面的情况下,必须将记录解释为描述指针序列及其对应的基指针。如果位置的大小为 N x sizeof(pointer),则位置内将包含 N 个每个指针一个的记录。可以假设一对中的两个位置大小相同。

请注意,每个部分中使用的位置可能描述相同的位置。例如,栈槽可能显示为 deopt 位置、gc 基指针和 gc 派生指针。

对于状态点记录,StkMapRecord 的 LiveOut 部分将为空。

安全点语义和验证

编译代码相对于垃圾收集器正确性的基本正确性属性是动态的。必须存在这样的情况,即没有动态跟踪使得涉及潜在重新定位指针的操作在可以重新定位它的安全点之后可观察到。“可观察之后”在此用法中意味着外部观察者可以以一种排除操作在安全点之前执行的方式观察此事件序列。

为了理解为什么需要这个“observable-after”属性,考虑对重新定位指针的原始副本执行空比较。假设控制流遵循安全点,则无法从外部观察空比较是在安全点之前还是之后执行的。(记住,原始值不会被安全点修改。)编译器可以自由地做出任一调度选择。

实际实现的正确性属性比这稍微强一些。我们要求在任何静态路径上,潜在的重新定位指针都不会“observable-after”它可能已经被重新定位。这比严格必要的稍微强一些(因此可能会禁止一些原本有效的程序),但大大简化了对编译代码正确性的推理。

根据构造,如果在源 IR 中正确建立,优化器将维持此属性。这是设计的一个关键不变式。

现有的 IR 验证器传递已被扩展以检查其各自文档中提到的内联函数的大多数本地限制。LLVM 中的当前实现不检查关键的重新定位不变式,但正在进行开发此类验证器的相关工作。如果您有兴趣尝试当前版本,请在 llvm-dev 上提问。

安全点插入的实用程序传递

RewriteStatepointsForGC

RewriteStatepointsForGC 传递将函数的 IR 转换为从上面描述的抽象机器模型降低到显式状态点重新定位模型。为此,它将所有可能包含安全点轮询的函数调用或调用替换为gc.statepoint和相关的完整重新定位序列,包括所有必需的gc.relocates

此传递仅适用于设置了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
}

此传递将生成以下 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-examplecoreclr策略(支持状态点的仅有的两个默认策略)都使用 addrspace(1) 来确定哪些指针是引用,但是自定义策略不必遵循此约定。

此传递可以用作语言前端的实用程序函数,该函数前端不希望在构造 IR 时手动推理活动性、基指针或重新定位。在当前实现中,RewriteStatepointsForGC 必须在 SSA 构造(即 mem2ref)之后运行。

RewriteStatepointsForGC 将确保为创建的每个重新定位列出合适的基指针。它将通过根据需要复制代码来实现此目的,以将与每个要重新定位的指针关联的基指针传播到相应的安全点。实现假设以下 IR 结构生成基指针:来自堆的加载、全局变量的地址、函数参数、函数返回值。常量指针(如空指针)也被假定为基指针。在实践中,此约束可以放松为生成内部派生指针,前提是目标收集器可以从任意内部派生指针找到关联的分配。

默认情况下,RewriteStatepointsForGC 将0xABCDEF00作为状态点 ID 和0作为可修补字节数传递给新构造的gc.statepoint。这些值可以使用属性"statepoint-id""statepoint-num-patch-bytes"在每个调用站点上进行配置。如果调用站点用"statepoint-id"函数属性标记,并且其值为正整数(表示为字符串),则该值用作新构造的gc.statepoint的 ID。如果调用站点用"statepoint-num-patch-bytes"函数属性标记,并且其值为正整数,则该值用作新构造的gc.statepoint的“可修补字节数”参数。"statepoint-id""statepoint-num-patch-bytes"属性不会传播到gc.statepoint调用或调用,如果它们可以成功解析。

在实践中,RewriteStatepointsForGC 应该在传递管道中运行得晚得多,在大多数优化已经完成之后。这有助于在启用垃圾收集支持的情况下编译时提高生成的代码质量。

RewriteStatepointsForGC 内联函数降低

作为降低到显式重新定位模型的一部分,RewriteStatepointsForGC 对以下内联函数执行 GC 特定的降低

  • gc.get.pointer.base

  • gc.get.pointer.offset

  • llvm.memcpy.element.unordered.atomic.*

  • llvm.memmove.element.unordered.atomic.*

memcpy 和 memmove 操作有两种可能的降低方式:GC 叶子降低和 GC 可解析降低。如果调用显式标记有“gc-leaf-function”属性,则该调用将降低到对“__llvm_memcpy_element_unordered_atomic_*”或“__llvm_memmove_element_unordered_atomic_*”符号的 GC 叶子调用。此类调用不能采用安全点。否则,通过将调用包装到状态点中,使调用可由 GC 解析。这使得在复制操作期间可以采用安全点。请注意,GC 可解析复制操作不需要采用安全点。例如,短复制操作可以在不采用安全点的情况下执行。

对“llvm.memcpy.element.unordered.atomic.*”、“llvm.memmove.element.unordered.atomic.*”内联函数的 GC 可解析调用分别降低到对“__llvm_memcpy_element_unordered_atomic_safepoint_*”、“__llvm_memmove_element_unordered_atomic_safepoint_*”符号的调用。通过这种方式,运行时可以提供带和不带安全点的复制操作的实现。

GC 可解析降低还涉及调整调用的参数。Memcpy 和 memmove 内联函数采用派生指针作为源和目标参数。如果复制操作采用安全点,则可能需要重新定位底层源和目标对象。这需要在复制操作中提供相应的基指针。为了使基指针可用,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 传递插入足够的安全点轮询,以确保正在运行的代码及时检查安全点请求。此传递预计在 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
}

此传递将生成以下 IR

define void @test() gc "statepoint-example" {
  call void @do_safepoint()
  call void @foo()
  ret void
}

在这种情况下,我们添加了一个(无条件的)入口安全点轮询。请注意,尽管外观如此,入口轮询不一定是冗余的。我们必须知道footest不是相互递归的,轮询才冗余。在实践中,您可能希望您的轮询定义包含某种形式的条件分支。

目前,PlaceSafepoints 可以在方法入口和循环回边位置插入安全点轮询。如果需要,将其扩展到与返回轮询一起使用将非常简单。

PlaceSafepoints 包含许多优化,以避免在特定站点放置安全点轮询,除非需要确保在正常情况下及时执行轮询。PlaceSafepoints 不会尝试确保在最坏情况下(例如系统大量分页)及时执行轮询。

安全点轮询操作的实现是通过在包含的模块中查找名为gc.safepoint_poll的函数来指定的。此函数的主体将插入到所需的每个轮询站点。虽然此方法内的调用或调用将转换为gc.statepoints,但不会执行递归轮询插入。

此传递对于只需要在安全点支持垃圾收集语义的任何语言前端都很有用。如果您需要在安全点处获取其他抽象帧信息(例如用于反优化或内省),则可以在前端插入安全点轮询。如果您有后一种情况,请在 llvm-dev 上寻求建议。在实践中,已经做了很多工作来使这样的方案正常工作,但尚未在此处记录。

支持的架构

对状态点生成的支持需要每个后端的一些代码。目前,仅支持 Aarch64 和 X86_64。

局限性和半成品想法

混合引用和原始指针

在抽象机器模型中支持允许非托管指针指向垃圾回收对象的语言(即,将指向对象的指针传递给 C 例程)。目前,关于如何处理此问题的最佳想法涉及隐藏引用值和原始指针之间连接的内联函数或不透明函数。问题在于,使用 ptrtoint 或 inttoptr 转换(在这些用例中很常见)违反了在从抽象模型降低到显式物理模型时,用于推断任意引用的基指针的规则。请注意,直接降低到物理模型的前端在这里没有任何问题。

堆栈上的对象

如上所述,显式降低支持在堆栈上分配的对象,前提是收集器可以在给定堆栈地址的情况下找到堆映射。

缺少的部分是 a) 与从抽象机器模型重写 (RS4GC) 的集成,以及 b) 对可选地分解堆栈上的对象的支持,以便不需要为其提供堆映射。后者是为方便与某些收集器集成而必需的。

降低质量和表示开销

目前的状态点降低已知质量较差。从长远来看,我们希望将状态点与寄存器分配器集成;在短期内,这不太可能发生。我们发现降低的质量相对不重要,因为热状态点几乎总是内联器错误。

有人提出,状态点表示会导致某些示例生成大量的IR,并且这会导致内存使用量和编译时间高于预期。目前没有立即计划对此进行更改,但将来可能会探索其他模型。

异常边上的重定位

当前,ToT 中异常路径上的重定位已损坏。特别是,目前无法表示在也具有重定位的路径上的重新抛出。有关更多详细信息,请参阅此 llvm-dev 讨论

错误和增强功能

目前已知的错误和正在考虑的增强功能可以通过在摘要字段中执行bugzilla 搜索[Statepoint] 来跟踪。在提交新的错误时,请使用此标签,以便相关人员看到新提交的错误。与大多数 LLVM 功能一样,设计讨论在Discourse 论坛上进行,补丁应发送到llvm-commits以供审查。