LLVM中垃圾收集安全点

原文:http://llvm.org/docs/Statepoints.html

状态

本文档描述了LLVM的一组实验性扩展。小心使用,因为这些固有函数是实验性的。LLVM发布间的兼容也是不保证的。

对支持使用gcroot固有函数的保守垃圾收集,LLVM目前支持另一种机制。这里描述的机制与gcroot实现很少有相同的地方,这个机制被寄予厚望,将最终替换gcroot机制。

概观

为了收集死去的对象,垃圾收集器必须能够识别包含在执行代码中对象的任何引用,并且依赖于收集器,可能更新它们。收集器不是在代码中的所有地方都需要这个信息——这将极大增加问题的难度——仅是在执行中称为‘安全点’(safepoint)的定义良好的点。对大多数收集器,追踪每个唯一指针值的至少一个拷贝就足够。不过,对于希望重定位运行中代码可直接到达对象的收集器,要求一个更高的标准。

另一个挑战是,编译器可能会计算指向该分配以外,甚至指向另一个分配内部,的中间结果(派生指针)。这个中间结果的最终使用必须产生该分配边界内的一个地址,但这样的“外部派生指针”可能对收集器是可见的。

有鉴于此,一个垃圾收集器不能安全地依赖一个地址的运行时值来表示它所关联的对象。如果垃圾收集器希望移动任何对象,对每个指针,编译器必须对其分配标示提供一个映射。

为了简化收集器与编译后代码之间的交互,大多数垃圾收集器依据3个抽象来组织:读栅栏(loadbarriers),写栅栏(storebarriers),以及安全点(safepoint)。

1.      读栅栏是紧跟机器读指令后,但在读入值被使用之前执行的一小块代码。依赖于收集器,这样一个栅栏可能对于所有的读操作都是需要的,仅(在源代码语言里)特定类型的读操作可以幸免。

2.      类似地。写栅栏是在机器写指令之前,但在写入值计算之后运行的一个代码片段。写栅栏最常见的使用是在一个生成的垃圾收集器中更新一个‘卡表’(cardtable)。

3.      安全点是允许改变对编译后代码可见的指针(即当前在寄存器或栈上)的一个位置。在安全点完成后,实际指针值可能会不同,但(从源代码语言看来)所指向的‘对象’不变。

注意术语‘安全点’有点使用过度。它同时指使机器状态可解析的位置,以及涉及使应用程序线程到达收集器可以安全使用该信息(译注:指机器状态信息)地点的协作协议。在本文中,术语“statepoint”仅仅指前者。

本文关注最后一条——编译器在生成代码里对安全点的支持。我们将假定一个外部的机制已经决定在何处放置安全点。在我们看来,所有的安全点都将是函数调用。为了支持在编译后代码中对从值可到达对象的重定位,收集器必须能够:

1.      在安全点识别一个指针的每个拷贝(包括由编译器本身引入的拷贝),

2.      识别每个指针相关的对象,并且

3.      可能更新每个拷贝。

本文描述一个机制,基于LLVM的编译器可用来向一个语言运行时、收集器提供这个信息,并确保如果需要,所有的指针都可以读及更新。该方法的核心是以一个方式构造(或重写)IR,使垃圾收集器可能执行的更新在IR中显式可见。这样做要求我们:

1.      对每个潜在被重定位的指针创建一个新的SSA值,确保在该安全点后没有对原始(非重定位)值的使用,

2.      以编译器不透明的方式指定该重定位,确保优化器在一个statepoint后不能引入一个非重定位值新的使用。这阻止优化器执行不可靠的优化。

3.      对每个statepoint记录活动指针的一个映射(及相关的内存分配)。

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

注意,任务:识别所有指向被垃圾收集值的指针;转换IR以显露一个指针,为每个这样的活动指针给出基本对象;并正确地插入所有的固有函数;明显超出了本文的范畴。建议的做法是使用下面描述的实用遍(utilitypass)。

这个抽象函数调用具体地由一系列统称为“statepoint重定位序列”的固有函数调用来表示。

让我们考虑LLVM IR中的一个简单调用:

define i8 addrspace(1)* @test1(i8 addrspace(1)* %obj)

       gc "statepoint-example" {

  call void ()* @foo()

  ret i8 addrspace(1)* %obj

}

依赖于语言,可能需要在foo的执行期间酌加一个安全点。如果这样,我们需要让收集器更新当前帧里的局部值。如果不这样做,一旦最终从这个调用返回,我们将访问一个可能无效的引用。

在这个例子里,我们需要重定位SSA值%obj。因为我们不能实际地改变SSA值%obj中的值,我们需要引入一个代表%obj在这个安全点后潜在改变值的新SSA值%obj.relocated,并恰当地更新随后的使用。得到的重定位序列是:

definei8 addrspace(1)* @test1(i8 addrspace(1)* %obj)

       gc "statepoint-example" {

  %0 = call token (i64, i32, void ()*, i32,i32, ...)* @llvm.experimental.gc.statepoint.p0f_isVoidf(i64 0, i32 0, void ()*@foo, i32 0, i32 0, i32 0, 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

}

理想地,这个序列将被表示为一个M个实参,N个返回值的函数(其中M是要重定位值的个数+原始的调用实参,N是原始返回值+被重定位值的个数),但LLVM支持这样一个表示并不容易。

作为替代,statepoint固有函数标记安全点或statepoint的实际调用点。Statepoint返回一个(仅存在于编译时刻的)符号值。为了取回该调用原始的返回值,我们使用gc.result固有函数。为了依次得到每个指针的重定位,我们以合适的索引使用gc.relocate固有函数。注意gc.relocate与gc.result都与该statepoint紧密相连。该组合构成了一个“statepoint重定位序列”,代表一个可解析的调用或‘statepoint’的整体。

在降级时,这个例子将生成以下的x86汇编代码:

        .globl        test1

        .align        16, 0x90

        pushq %rax

        callq foo

.Ltmp1:

        movq  (%rsp), %rax  #This load is redundant (oops!)

        popq  %rdx

        retq

每个潜在的重定位值已经被溅出到栈,该位置的一个记录已经被记录到栈映射节。如果垃圾收集器需要在该调用期间更新所有这些指针,它确切知道要改什么。

我们例子StackMap节的相关部分是:

#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 valuewas 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.relocate提供一个显式操作数来描述与一个派生指针相关的内存分配。这个操作数通常被称为基本操作数,不严格要求必须是一个基本指针,但它必需位于相关内存分配边界内。某些收集器可能要求该操作数是一个实际的基本指针,而不仅是一个内部的派生指针。注意在降级期间,要求基本与派生指针的操作数在相关调用安全点后存活,即使基本指针随后不再使用。

如果将前面的例子扩展为包括一个无意义的派生指针,我们得到:

definei8 addrspace(1)* @test1(i8 addrspace(1)* %obj)

       gc "statepoint-example" {

  %gep = getelementptr i8, i8 addrspace(1)*%obj, i64 20000

  %token = call token (i64, i32, void ()*, i32,i32, ...)* @llvm.experimental.gc.statepoint.p0f_isVoidf(i64 0, i32 0, void ()*@foo, i32 0, i32 0, i32 0, i32 0, i8 addrspace(1)* %obj, i8 addrspace(1)* %gep)

  %obj.relocated = call i8 addrspace(1)*@llvm.experimental.gc.relocate.p1i8(token %token, i32 7, i32 7)

  %gep.relocated = call i8 addrspace(1)*@llvm.experimental.gc.relocate.p1i8(token %token, i32 7, i32 8)

  %p = getelementptr i8, i8 addrspace(1)* %gep,i64 -20000

  ret i8 addrspace(1)* %p

}

注意在这个例子里%p与%obj.relocate的地址相同,我们可以其中一个替换另一个,有机会在安全点从活动集中完全移除这个派生指针。

GC转变(transition)

作为一个实际的考虑,许多垃圾收集系统允许收集器感知代码(“托管代码”)调用非收集器感知代码(“非托管代码”)。通常这样的调用必须也是安全点,因为在非托管代码执行期间允许收集器运行是有利的。另外,协调从托管代码到非托管代码的转变,通常要求在调用点生成额外的代码,以通知收集器这个转变。为了支持这些需求,一个statepoint可以被标记为一个GC转变,执行该转变所需的数据(如果有)可以提供为该statepoint的额外实参。

注意虽然在许多情形下,statepoint可能指基于所涉及函数符号的GC转变(比如带有GC策略“foo”的一个函数调用带有GC策略“bar”的一个函数),同时GC转变的间接调用也必须支持。这个要求是规定显式标记GC转变的决策的背后驱动力。

让我们重温上面的例子,这次将@foo的调用处理作一个GC转变。依赖目标机器,转变代码可能需要访问某些额外的状态以通知收集器这个转变。让我们假设一个名字没什么创意的假想GC——“hypothetical-gc”——要求在一个对非托管代码进行调用的前后,必须写一个TLS变量。得到的重定位序列是:

@flag= thread_local global i32 0, align 4

 

definei8 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转变被发布为单个空操作。这些空操作通常在死代码消除期间由后端删除。

固有函数

llvm.experimental.gc.statepoint

语法:

declaretoken

  @llvm.experimental.gc.statepoint(i64<id>, i32 <num patch bytes>,

                 func_type <target>,

                 i64 <#call args>, i64<flags>,

                 ... (call parameters),

                 i64 <# transition args>,... (transition parameters),

                 i64 <# deopt args>, ...(deopt parameters),

                 ... (gc parameters))

【译注:上面的@llvm.experimental.gc.statepoint.p0f_isVoidf是这个固有函数的一个重载版本,后缀p0f_isVoidf表示该statepoint中调用的函数为void()类型。另外,这里给出的参数列表与例子也不尽相同,盖因例子来自早期版本的文档,没有进行相应的更新】。

概观:

Statepoint固有函数代表一个可由运行时解析的调用。

操作数:

操作数id是一个整数常量,它被转述为生成栈映射中的ID域。LLVM不解释这个参数,其含义由statepoint的使用者决定。注意LLVM可以任意复制包含statepoint调用的代码,这可能将statepoint每字面调用对应一个唯一id的IR转变为不再唯一。

如果num patch bytes不为0,不再发布对应该statepoint的调用指令,取而代之,LLVM发布numpatch bytes个空操作。LLVM将根据调用惯例,发布准备函数实参以及获取函数返回值的代码;前者在空操作序列前,后者在后。预期用户在执行生成的机器代码前,将numpatch bytes个字节的空操作修补为特定于他们运行时的调用序列。至于空操作序列的对齐,则没有保证。不同于LLVM里的栈映射与补丁点,statepoint没有影子字节的概念。注意在语义上statepoint仍然代表对target的一个call或invoke,修补后的空操作序列预期代表等同于target的一个call或invoke的操作。

操作数target是实际要调用的函数。可使用一个符号化的LLVM函数,或合适函数类型的任一Value来指定target。注意函数类型必须匹配被调用者的署名,以及callparameters实参的类型。

操作数#call args是实际调用的实参个数。它必须严格匹配在callparameters可变长部分传入的实参个数。

操作数flags用于指定statepoint的额外信息。目前这仅用于将特定的statepoint标记为GC转变。这个操作数是一个带有以下布局的64位整数,其中比特位0是最低位:

比特位 #

使用

0

如果statepoint是一个GC转变,设置它,否则清除。

1-63

为将来保留,必须为0

实参call parameters只是需要传递给调用目标的实参。它们将根据指定的调用惯例降级,否则像一个普通调用指令那样处理。实参的个数必须严格与#callargs匹配。类型必须匹配target的署名。

实参transition parameters包含需要传递给GC转变代码的一个任意长Value列表。它们将被降级,并作为操作数传递给指令选择DAG中合适的GC_TRANSITION节点。这些实参被认为在被调用者的执行前后一定可用(但在执行期间不一定)。#transitionargs表示有多少操作数需要被解释为transitionparameters。

实参deopt parameters包含对运行时有意义的任意长Value列表。运行时可能会读其中任意值,但被认为不会修改它们。如果垃圾收集器可能需要修改其中一个值,它还必须列出在gcpointer实参列表中。#deoptargs表示有多少操作数需要被解释为deoptparameters。

实参gc parameters包含每个可能需要垃圾收集器更新,指向垃圾收集器对象的指针。注意这个实参列表必须对每个列出的派生指针显式包含一个基本指针。参数的次序不重要。不像其他变长参数集,这个列表没有长度前缀。

语义:

一个statepoint被认为读写所有的内存。结果,内存操作重排不能越过一个statepoint。将一个statepoint标记为readonly或readnone是非法的。

注意在这个statepoint静态可到达的一个位置处,合法的IR不能在它的一个gcpointer实参上执行任何内存操作。作为替代,必须使用显式重定位后的值(来自一个gc.relocate)。

llvm.experimental.gc.result

语法:

declaretype*

  @llvm.experimental.gc.result(token%statepoint_token)

概观:

Gc.result提取被gc.statepoint替换的原始调用指令的结果。由于实现的限制,固有函数gc.result实际上是3个固有函数组成的一个家族。除了返回值类型,语义都是相同的。

操作数:

第一个且仅有的实参是开启这个gc.result作为部分的安全点序列的gc.statepoint。尽管它的类型作为一个通用符号,这里只有由一个gc.statepoint定义的值是合法的。

语义:

Gc.result代表statepoint调用目标的返回值。Gc.result的类型必须严格匹配这个目标的类型。如果这个调用目标返回void,将没有gc.result。

gc.result被塑造为一个readnone的纯函数。它没有副作用,因为它只是由gc.statepoint代表的之前调用的返回值的一个投射(projection)。

llvm.experimental.gc.relocate

语法:

declare<pointer type>

  @llvm.experimental.gc.relocate(token%statepoint_token,

                                 i32%base_offset,

                                 i32%pointer_offset)

概观:

Gc.relocate返回在安全点处一个指针潜在的重定位值。

操作数:

第一个实参是开启这个gc.relocation作为部分的安全点序列的gc.statepoint。尽管它的类型作为一个通用符号,这里只有由一个gc.statepoint定义的值是合法的。

第二个实参是指定用于指针重定位的内存分配的statepoint实参列表的一个索引。这个索引必须落在这个statepoint实参列表的gcparameter部分。所关联的值必须在要重定位指针所关联对象的内部。优化器可以不受限制地改变被转述的内部派生指针,只要它不使用另一个内部派生指针替换一个真正的基本指针。允许收集器依赖这样的假设:如果这样构建,基本指针操作数保持为一个真正的基本指针。

第三个实参是指定(潜在)要重定位的派生指针的statepoint实参列表的一个索引。当且仅当一个基本指针要被重定位,这个索引与第二个实参相同是合法的。这个索引必须落在该statepoint实参列表的gcparameter部分。

语义:

Gc.relocate的返回值是由其实参指定的指针的潜在重定位后的值。没有指定返回指针的值如何关联到gc.statepoint的实参,除了a)它指向同一个源代码语言对象的相同偏移,b)新近重定位指针的‘基于’关系是非重定位指针的一个投射。特别的,返回指针的整数值是未指定的。

Gc.relocate被塑造为一个readnone的纯函数。它没有副作用,因为它只是提取关于由这个gc.statepoint塑造的实际调用所完成工作的信息的一个方法。

栈映射格式

每个可能由运行时或收集器读与/或更新的指针值的Location,通过在补丁点文档中说明的栈映射格式来提供。

每个statepoint产生以下Location:

·        描述调用目标调用惯例的常量。这个常量是产生这个栈映射的LLVM版本的一个有效的调用惯例标识。对这个常量不会有超出LLVM在别处就这些标识符所给出的兼容性保证。

·        描述传递给这个statepoint固有函数的标记的常量。

·        描述随后deopt位置(不是操作数)个数的常量

·        可变数量的Location,每个列出在IRstatepoint的deopt参数一个(与前面常量描述的数目相同)。目前,仅支持宽度不超过64比特的deopt参数。一个超过64比特类型的值可以指定和转述,只要a)在调用点这个值是常量,且b)这个常量可以不超过64比特来表示(假定0扩展到原来的宽度)。

·        可变数量的重定位记录。每个记录包含两个Location。重定位记录在下面详细描述。

对收集器重定位一个或多个派生指针,每个重定位记录提供了足够的信息。每个记录包含一对Location。记录中的第二个元素代表需要更新的指针(或多个指针)。记录中第一个元素提供了与要重定位指针关联的对象基址的一个指针。对处理广义的派生指针这个信息是必需的,因为指针可能在原始内存分配的边界以外,但仍然需要连同该内存分配进行重定位。另外:

·        如果用在statepoint之后,基本指针还必须显式地作为一个重定位对出现是得到保证的。

·        IR statepoint中重定位记录可能少于gc参数。每个唯一的对将至少出现一次;重复是可能的。

·        每个记录里的Location可能是一个或多个指针的大小。对后者,记录必须被解释为描述一个指针序列以及对应的基本指针。如果Location是N xsizeof(pointer)大小,那么将会有N个记录与包含在这个Location里的指针一一对应。可以假定在一个对里的两个Location具有相同大小。

注意在每个节中使用的Location可能描述相同的物理位置。比如,一个栈槽可能显示为一个deopt位置,一个gc基本指针,以及一个gc派生指针。

对一个statepoint记录,StkMapRecord的LiveOut节将是空的。

安全点语义与验证

就垃圾收集器而言,代表编译后代码正确性的基本正确性属性是动态的。它必须是这样的情形:没有动态的线索使得涉及一个潜在重定位指针的操作是在一个可以重定位它的安全点后面可见(observably-after)。‘后面可见’是这个用法:意味着一个外部的观察者可,以一个阻止该操作在这个安全点前执行的方式,观察到这系列的事件

为了理解为什么这个‘后面可见’属性是必需的,考虑在一个重定位后指针的原始拷贝上执行的空指针比较。假定控制流接着这个安全点,没有方法从外部观察到这个空指针比较是否在该安全点的前后执行。(记住,安全点没有修改原始的Value)。编译器可以自由选择调度选项。

实际实现的正确性属性比这稍严格一些。我们要求没有这样的静态路径:一个潜在重定位的指针在它可能已经被重定位后是‘后面可见’的。这比严格必要条件稍严格一些(因此可能禁止某些原本合法的程序),但极大地简化了对编译后代码正确性的推理。

通过构造,如果正确地建立在源IR中,优化器将支持这个属性。这是这个设计关键的不变量。

已经扩展现存的IR验证器遍来检查大多数在各自文档中提及的固有函数的局部约束。LLVM的当前实现不检查关键的重定位不变量,但正在开发这样一个验证器。如果你对使用当前版本进行试用感兴趣,请咨询llvm-dev。

用于安全点插入的功用遍

RewriteStatepointsForGC

遍RewriteStatepointsForGC通过将一个gc.statepoint(带有一个可选的gc.result)替换为一个完整的重定位序列,包括所有必需的gc.relocate,来转换一个函数IR。就函数而言,这个遍要求,在给定IR中,为该函数指定的GC策略能够可靠地区分GC引用与非GC引用。

作为一个例子,考虑这个代码:

definei8 addrspace(1)* @test1(i8 addrspace(1)* %obj)

       gc "statepoint-example" {

  call token (i64, i32, void ()*, i32, i32,...)* @llvm.experimental.gc.statepoint.p0f_isVoidf(i64 2882400000, i32 0, void()* @foo, i32 0, i32 0, i32 0, i32 5, i32 0, i32 -1, i32 0, i32 0, i32 0)

  ret i8 addrspace(1)* %obj

}

这个遍将生成这个IR:

definei8 addrspace(1)* @test1(i8 addrspace(1)* %obj)

       gc "statepoint-example" {

  %0 = call token (i64, i32, void ()*, i32,i32, ...)* @llvm.experimental.gc.statepoint.p0f_isVoidf(i64 2882400000, i32 0,void ()* @foo, i32 0, i32 0, i32 0, i32 5, i32 0, i32 -1, i32 0, i32 0, i32 0,i8 addrspace(1)* %obj)

  %obj.relocated = call coldcc i8 addrspace(1)*@llvm.experimental.gc.relocate.p1i8(token %0, i32 12, i32 12)

  ret i8 addrspace(1)* %obj.relocated

}

在上面的例子里,在指针上的address(1)标记是statepoint-exampleGC策略用来区分引用与非引用。出于这个目的,地址空间1不是全局预留的。

一个在构造IR时不希望人工推导生命期、基本指针、或重定位的语言前端可以将这个遍用作一个功用函数。正如当前实现的,RewriteStatepointsForGC必须在SSA构造后运行(比如mem2ref)。

RewriteStatepointsForGC将确保对创建的每个重定位列出合适的基本指针。这通过按需复制代码,将与每个要重定位的指针关联的基本指针传播到合适的安全点来完成。实现假定以下IR构造产生基本指针:从堆读入,全局变量的地址,函数实参,函数返回值。常量指针(比如null)也被认为是基本指针。在实践中,可以放松这个限制来生成内部派生指针,只要目标收集器可以从任意内部派生指针找到相关的内存分配。

在实践中,RewriteStatepointsForGC可以在遍流水线的更后面运行,在大多数优化已经完成之后。在支持垃圾收集编译时,这有助于提高生成代码的质量。长远来看,这是预期的使用模式。目前,仅制定出保证这总是正确所要求的语义模型的少量细节。因此,请小心使用并报告bug。

PlaceSafepoints

PlaceSafepoints遍通过以恰当的gc.statepoint及gc.result对替换任何call或invoke指令,并插入数目足以保证及时运行一个安全点请求的代码检查的安全点轮询,来转换一个函数的IR。这个遍被预期在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:

definevoid @test() gc "statepoint-example" {

  %safepoint_token = call token (i64, i32, void()*, i32, i32, ...)* @llvm.experimental.gc.statepoint.p0f_isVoidf(i642882400000, i32 0, void ()* @do_safepoint, i32 0, i32 0, i32 0, i32 0)

  %safepoint_token1 = call token (i64, i32,void ()*, i32, i32, ...)* @llvm.experimental.gc.statepoint.p0f_isVoidf(i642882400000, i32 0, void ()* @foo, i32 0, i32 0, i32 0, i32 0)

  ret void

}

在这个情形里,我们添加了一个(非条件)入口安全点轮询,并将call转换为一个gc.statepoint。注意摈除表象,这个入口轮询不一定是多余的。我们必须确认foo与test不是相互递归的,这个轮询才是多余的。在实践中,你可能希望你的轮询定义包含一个某种形式的条件跳转。

目前,PlaceSafepoints可以在方法入口及循环回边等位置插入安全点轮询。如果需要,扩展来处理返回轮询也是简单明了的。

PlaceSafepoints包括若干避免在特定地点放置安全点轮询的优化,除非需要确保在正常条件下及时执行一个轮询。PlaceSafepoints不会尝试确保在最坏情形下,比如繁重的系统换页,轮询的及时执行。

安全点类型活动的实现通过在所在模块中查阅一个名为gc.safepoint_poll的函数来指定。这个函数的主体被插入每个期望的轮询点。虽然这个方法里的call或invoke会转换为一个gc.statepoint,但不会递归插入轮询。

默认地PlaceSafepoints将向新创建的gc.statepoint传递0xABCDEF00 作为statepointID,0作为可修补字节数。这些值可以使用属性statepoint-id与statepoint-num-patch-bytes在每调用点基础上配置。如果一个调用点被一个值是正整数(表示为一个字符串)的statepoint-id函数属性标记,那么该值被用作新创建gc.statepoint的ID。如果一个调用点被一个值是正整数的statepoint-num-patch-bytes函数属性标记,那么该值被用作新创建gc.statepoint的numpatch bytes参数。如果能成功解析,属性statepoint-id与statepoint-num-patch-bytes不会传播到gc.statepoint的call或invoke。

如果你安排RewriteStatepointsForGC遍在后执行,你可能应该把这个遍安排在它前面。例外是如果你需要保留在安全点的抽象帧信息(比如用于去优化(deoptimization)或自我训练(introspection))。在这个情形下,应该向llvm-dev邮件列表中寻求建议。

支持的架构

对statepoint生成的支持对每个后端都要求一些代码。目前,仅支持X86_64。

问题领域及活跃工作

1.       随着最近重写模型的现有用户的成熟,我们发现了优化器破坏了假设:gc-pointer类型的一个SSA值实际包含一个gc-pointer,反之亦然,的情形。我们需要澄清我们的预期并建议至少一个小的IR修改。(目前,gc-pointer特征通过地址空间管理。这被证明不够强壮)。

2.       对通过固定(pinning)允许非托管指针指向垃圾收集对象(比如向一个C例程传递指向一个对象的指针)的语言的支持。

3.       对分配在栈上垃圾收集对象的支持。特别地,alloca总是假定在地址空间0里,我们需要一个cast/promotion操作符来让重写识别它们。

4.       当前的statepoint降级已知有时是很差的。在非常长的时期,我们倾向于整合statepoint与寄存器分配;在近期,这不太可能发生。我们已经发现降级的质量相对不重要,因为烫手的statepoint几乎总是内联器的bug。

5.       有人提出要关注对某些例子,statepoint表示导致产生大量的IR,这带来比预期更高的内存使用以及更长的编译时间。对此,没有立即的修改计划,但在将来可能会探究替换模型。

6.       目前在ToT中异常路径上的重定位是坏的。特别的,目前没有方法表示在一条路径上的一个也带有重定位的重新抛出。更多细节参考这个llvm-dev讨论

Bug与增强

目前已知的bug与考虑中增强可以通过在汇总域执行对[Statepoint]一个bugzilla查找来追踪。在报告新bug时,请使用这个标签以使感兴趣的同行看到新报告的bug。正如大多数LLVM专题,设计讨论在llvm-dev进行,补丁应该发送到llvm-commits来复审。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值