原文:http://llvm.org/docs/GarbageCollection.html
摘要
本文论及如何将LLVM整合进一个支持垃圾收集语言的编译器。注意LLVM本身不支持垃圾收集。你必须自己提供。
快速开始
首先,你应该选择一个收集器策略。LLVM包括若干内置策略,但你还可以一个定制的定义来实现一个可载入的插件。注意收集器策略是一个LLVM应该如何生成代码,使它与你的收集器及运行时交互的描述,而不是对收集器本身的描述。
其次,将你生成的函数标记为使用你选择的收集器策略。在C++代码中,你可以调用:
F.setGC(<collector descriptionname>);
这将产生类似如下片段的IR:
define void @foo() gc "<collectordescription name>" { ... }
在为你的函数生成LLVM IR时,你将需要:
· 使用@llvm.gcread及、或@llvm.gcwrite替换标准读写(load与store)指令。这些固有函数用于表示读写栅栏。如果你的收集器不要求这样的栅栏,你可以跳过这步。
· 使用你的垃圾收集器运行时库提供的内存分配例程。
· 如果你的收集器要求,根据你的运行时库接口,生成类型映射。LLVM不涉及这个过程。特别的,LLVM类型系统不适合在编译器中传达这样的信息。
· 插入任何与你的收集器交互所需的协调代码。许多收集器要求定期运行应用程序代码检查一个标记,有条件地调用一个运行时函数。这通常称为安全点轮询(safepointpoll)。
你需要在你生成的IR中识别根(即你的收集器需要知道的对堆对象的引用),这样LLVM可以将它们编码进你最终的栈映射(stackmap)。依赖于选中的收集器策略,这通过使用固有函数@llvm.gcroot或一个gc.statepoint重定位序列来完成。
不要忘了为每个在对一个表达式求值时生成的中间值创建一个根。在h(f(),g())里,如果对g()的求值触发了一个收集,很容易收集f()的结果。
最后,你需要将你的运行时与生成的可执行程序链接(对于静态编译器)或确保对运行时链接器有合适的符号可用(对于一个JIT编译器)。
介绍
什么是垃圾收集
垃圾收集是一个广泛使用的技术,它将程序员从必须知道堆对象的生命期中解放出来,使得软件更容易编写及维护。许多编程语言依赖于垃圾收集来进行自动内存管理。垃圾收集有两个主要形式:保守(conservative)与精确(accurate)。
保守的垃圾收集通常不要求语言或编译器任何特殊的支持:它可以处理非类型安全编程语言(比如C/C++),不从编译器要求任何特殊的信息。Boehm收集器是最先进保守收集器的一个例子。
精确垃圾收集要求在运行时识别程序中所有指针的能力(在大多数情形下这要求源语言是类型安全的)。在运行时识别指针,要求编译器的支持来定位所有在运行时持有活动指针变量的位置,包括处理器栈与寄存器。
保守垃圾收集是吸引人的,因为它不要求任何特殊的编译器支持,但它有疑难问题。特别的,因为保守垃圾收集不能知道在机器中一个特定的字是指针,它不能移动堆里的活动对象(阻碍了压缩与分代GC算法的使用),并且因为程序中恰好指向对象的整数值,它偶尔会遭遇内存泄露。另外,某些激进的编译器转换会破坏保守垃圾收集器(虽然在实践中这看起来很罕见)。
精确垃圾收集器没有这些问题,但它们会遭遇程序的标量优化退化(degradedscalar optimization)。特别的,因为运行时必须能够识别、更新所有程序中活跃的指针,某些优化会不那么有效。不过在实践中,局部性与使用激进垃圾收集技术对性能提升超过在底层的损失。
本文描述由LLVM提供的支持精确垃圾收集的机制与接口。
目标与非目标
LLVM的中间表示提供了支持许多收集器模型的垃圾收集固有函数。例如,这些固有函数容许:
· 半空间收集器(semi-space collectors)
· 标记清除收集器(mark-sweep collectors)
· 分代收集器(generational collectors)
· 递增收集器(incremental collectors)
· 并发收集器(concurrent collectors)
· 协作收集器(cooperative collectors)
· 引用计数(reference counting)
我们希望LLVM IR内置的支持足以支撑大量使用垃圾收集的语言,包括Scheme,ML,Java,C#,Perl,Python,Lua,Ruby,其他脚本语言,及更多。
注意LLVM本身不提供垃圾收集器——这应该是你语言运行时库的一部分。LLVM提供一个框架,向编译器描述垃圾收集器的要求。特别的,LLVM提供对在调用点生成栈映射(stackmap),轮询一个安全点,及发布读写栅栏的支持。你还可以扩展LLVM——通过一个可载入代码生成插件——来生成与运行时库规定的二进制接口兼容的代码与数据结构。这类似于LLVM与DWARF调试信息之间的关系。主要区别在于在垃圾收集领域缺乏已有的标准——因此需要一个灵活的扩展机制。
LLVM的GC支持所关注的二进制接口的方方面面有:
· 在代码中创建允许收集安全执行的GC安全点。
· 计算栈映射。对于代码中的每个安全点,必须识别在栈帧内的对象引用,这样收集器可以遍历并可能更新它们。
· 在写入堆对象引用时的写栅栏。这些通常用于优化分代收集器中的增量扫描。
· 在读入对象引用时发布读栅栏。这对并发收集器交互操作是有用的。
LLVM没有直接处理的额外领域有:
· 将全局根注册到运行时。
· 将栈映射入口注册到运行时。
· 程序用来分配内存、触发一个收集等的函数。
· 计算或编译类型映射(type map),或将它们注册到运行时。这用于在堆中爬行搜索对象引用。
通常,LLVM对GC的支持不包括可以被IR的其他特性适当满足的特性,并且不指定一个特定的二进制接口。在好的方面,这意味着你应该能够将LLVM与一个现存的运行时整合。另一方面,它给一个新语言的开发者留下了许多工作。通过提供可以与许多常用收集器设计一起工作的内置收集器策略描述与容易扩展的点,我们尝试减轻这些工作。如果你已经有一个需要支持的具体二进制接口,我们建议尝试使用其中一个内置收集器策略。
LLVM IR特性
本节描述由LLVM中间表示所提供的垃圾收集设施。这些IR特性的实际行为由选中的GC策略描述来指定。
指定GC代码生成:gc “…”
define<returntype> @name(...) gc "name" { ... }
gc函数属性用于向编译器指定期望的GC策略。其程序上等同于Function的方法setGC。
在一个函数上设置gc “name”触发了对匹配的GCStrategy子类的一个查找。某些收集器策略是内置的。你可以使用可载入插件机制,或者修补你的LLVM拷贝来添加其他。是选中的GC策略定义了支持GC的生成代码的实际属性。如果找不到,编译器将给出一个错误。
在每函数基础上指定GC形式允许LLVM将使用不同垃圾收集算法(或完全不使用)的程序链接起来。
识别栈上GC根
目前LLVM支持两个不同的机制来描述编译后代码在安全点的引用。Llvm.gcroot是旧的机制;gc.statepoint是新近添加的。目前,你可以选择其中一个实现(在每GC策略基础上)。长远来看,我们将可能完全移走llvm.gcroot,或大体上合并这两者。注意大多数新的开发工作围绕gc.statepoint。
使用gc.statepoint
这个页面包含了gc.statepoint的详细文档。
使用llvm.gcwrite
void @llvm.gcroot(i8** %ptrloc, i8* %metadata)
固有函数llvm.gcroot用于通知LLVM一个援引堆上对象的栈变量,出于垃圾收集的原因要追踪。对生成代码的实际影响由该Function选中的GC策略指定。所有对llvm.gcroot的调用都必须位于第一个基本块之中。
第一个实参必须援引一个alloca指令或其一个alloca的bitcast。第二个实参包含与该指针关联的元数据的一个指针,它必须是一个常量或全局值地址。如果你的目标收集器使用标签,使用元数据的一个空指针。
执行手动SSA构建的编译器必须确保,表示GC引用的SSA值,在每个调用点前,保存在传递给对应gcroot的alloca中,在每个调用之后重新读入。使用mem2reg得到在SSA中使用alloca的命令式代码的编译器,仅需对作为GC堆指针的那些变量添加对@llvm.gcroot的调用。
以llvm.gcroot标记中间值同样重要。例如,考虑h(f(),g())。在g()触发一次收集的情形下,小心f()结果的泄露。注意,栈变量必须在函数的prologue初始化并以llvm.gcroot标记。
实参%metadata可以用于避免要求堆对象具有isa指针或标签比特[Appel89,Goldberg91, Tolmach94] 。如果指定,将沿着栈帧中该指针的位置追踪其值。
考虑以下Java代码片段:
{
Object X; // A null-initialized reference to an object
...
}
这个块(它可能位于一个函数或一个循环的中间),可以编译为这个LLVM代码:
Entry:
;;In the entry block for the function, allocate the
;;stack space for X, which is an LLVM pointer.
%X = alloca %Object*
;;Tell LLVM that the stack space is a stack root.
;;Java has type-tags on objects, so we pass null as metadata.
%tmp = bitcast %Object** %X to i8**
call void @llvm.gcroot(i8** %tmp, i8* null)
...
;;"CodeBlock" is the block corresponding to the start
;; of the scope above.
CodeBlock:
;;Java null-initializes pointers.
store %Object* null, %Object** %X
...
;;As the pointer goes out of scope, store a null value into
;;it, to indicate that the value is no longer live.
store %Object* null, %Object** %X
...
堆中引用的读写
当修改者(mutator,需要垃圾收集的程序)从一个指针读写一个堆对象的一个域时,某些收集器需要得到通知。在这些点插入的代码片段分别被称为读栅栏与写栅栏。需要执行代码的数量通常很少,并且不在任何计算的关键路径上,因此栅栏对整体性能的影响可以接受。
栅栏通常要求访问对象指针,而不是派生指针(指向对象内部域的指针)。相应地,为完整起见,这些固有函数接受这两种指针作为独立的参数。在下面的片段里,%object是对象指针,而%derived是派生指针:
;;An array type.
%class.Array = type { %class.Object, i32, [0 x %class.Object*] }
...
;;Load the object pointer from a gcroot.
%object = load %class.Array** %object_addr
;;Compute the derived pointer.
%derived = getelementptr %object, i32 0, i32 2, i32 %n
LLVM不强制对象与派生指针间的这个关系(虽然一个特定的收集器策略可能会)。不过,可能会有违反它的不寻常的收集器。
如果目标GC不要求相应的栅栏,这些固有函数的使用自然是可选的。这样一个收集器使用的GC策略应该以相应的load或store指令替换所使用的固有函数调用。
当前设计一个已知的缺陷是栅栏固有函数不包括底下所执行操作的对齐大小。目前它假定操作是指针大小,对齐假定为目标机器的缺省对齐。
写栅栏:llvm.gcwrite
void @llvm.gcwrite(i8* %value, i8* %object, i8** %derived)
对于写栅栏,LLVM提供固有函数llvm.gcwrite。它具有与派生指针(第三个实参)的非易失性(non-volatile)store完全相同的语义。实际生成的代码由Function选中的GC策略指定。
许多重要的算法要求写栅栏,包括分代与并发收集器。另外,写栅栏可以用于实现引用计数。
读栅栏:llvm.gcread
i8* @llvm.gcread(i8* %object, i8** %derived)
对于读栅栏,LLVM提供固有函数llvm.gcread。它具有与派生指针(第二个实参)的非易失性load完全相同的语义。实际生成的代码由Function选中的GC策略指定。
需要读栅栏的算法要少于需要写栅栏的算法,并且可能具有更大的性能影响,因为读指针比写指针更频繁。
内置的GC策略
LLVM包括了对几种垃圾收集器的内置支持。
影子栈GC(The Shadow Stack GC)
要使用这个收集器策略,需要将你的函数标记为:
F.setGC("shadow-stack");
不像许多依赖一个协作代码生成器来编译栈映射的GC算法,这个算法小心地维护一个栈根的链表[Henderson2002]。这被称为“影子栈(shadowstack)”,机器栈的镜像。维护这个数据结构比使用编译进可执行代码作为常量数据的栈映射要慢,但有明显的可移植性的好处,因为它不要求目标机器代码生成器任何特殊的支持,也不要求在机器栈中充斥复杂、棘手的平台特定代码。
这个简单性与可移植性间的取舍是:
· 每函数调用的高开销。
· 非线程安全。
不过,它是一个容易开始的方式。在你的编译器及运行时起来、运行后,编写一个将允许你利用LLVM更先进的GC特性以提升性能的插件。
影子栈不意味着一个内存分配算法。一个半空间(semispace)收集器或构建顶层malloc都是好的出发点,可以很少的代码来实现。
不过,当开始收集时,你的运行时需要遍历栈根,对此它需要与影子栈集成。幸运地,这样做非常简单。(下面的代码包含大量注释以帮助你理解数据结构,实际仅有20行有意义的代码)。
///@brief The map for a single function's stack frame. One of these is
/// compiled as constant data into theexecutable for each function.
///
///Storage of metadata values is elided if the %metadata parameter to
///@llvm.gcroot is null.
struct FrameMap {
int32_t NumRoots; //< Number of roots in stack frame.
int32_t NumMeta; //< Number of metadata entries. May be < NumRoots.
const void *Meta[0]; //< Metadata foreach root.
};
///@brief A link in the dynamic shadow stack. One of these is embedded in
/// the stack frame of each function on thecall stack.
struct StackEntry {
StackEntry *Next; //<Link to next stack entry (the caller's).
const FrameMap *Map; //< Pointer toconstant FrameMap.
void *Roots[0]; //< Stack roots (in-place array).
};
///@brief The head of the singly-linked list of StackEntries. Functions push
/// and pop onto this in their prologue andepilogue.
///
///Since there is only a global list, this technique is not threadsafe.
StackEntry*llvm_gc_root_chain;
///@brief Calls Visitor(root, meta) for each GC root on the stack.
/// root and meta are exactly the values passedto
/// @llvm.gcroot.
///
///Visitor could be a function to recursively mark live objects. Or it
///might copy them to another heap or generation.
///
///@param Visitor A function to invoke for every GC root on the stack.
void visitGCRoots(void (*Visitor)(void **Root, const void *Meta)) {
for (StackEntry *R = llvm_gc_root_chain; R; R = R->Next) {
unsigned i = 0;
//For roots [0, NumMeta), the metadata pointer is in the FrameMap.
for (unsigned e = R->Map->NumMeta; i != e; ++i)
Visitor(&R->Roots[i], R->Map->Meta[i]);
//For roots [NumMeta, NumRoots), the metadata pointer is null.
for (unsigned e = R->Map->NumRoots; i != e; ++i)
Visitor(&R->Roots[i], NULL);
}
}
Erlang与Ocaml GCs
LLVM带有两个利用gcroot机制的例子收集器。就我们所知,没有任何语言的运行时实际使用它们,但对编写一个gcroot兼容GC插件感兴趣的人,它们确实提供了一个合理的出发点。特别的,它们是如何使用一个gcroot策略产生一个定制二进制栈映射格式仅有的在树中例子(Inparticular, these are the only in tree examples of how to produce a custombinary stack map format using a gcroot strategy)。
正如这些名字暗示的,生成的二进制格式是分别模仿Erlang与Ocaml编译器使用的格式。
Statepoint Example GC
F.setGC("statepoint-example");
这个GC提供了一个如何使用由gc.statepoint给出的基础设施的例子。这个例子GC与简化了gc.statepoint序列插入的PlaceSafepoints及RewriteStatepointsForGC应用遍兼容。如果你需要围绕gc.statepoint机制构建一个定制GC策略,建议你使用这个作为一个起点。
这个GC策略不支持读、写栅栏。因此,这些固有函数被降级为普通的读、写。
由这个GC策略生成的栈映射格式可以在使用这里记录的一个格式的栈映射节中找到。这个格式打算成为LLVM支持的标准格式。
CoreCLR GC
F.setGC("coreclr");
这个GC使用gc.statepoint机制来支持CoreCLR运行时。
对这个GC策略的支持是一个半成品。这个策略在某些方面不同于statepoint-exampleGC,比如:
· 不会显式追踪及报告内部指针的基础指针。
· 使用不同的格式编码栈映射。
· 仅在循环回边与尾调用之前需要安全点轮询(在函数入口不需要)。
定制GC策略
如果上面的内置GC策略描述不符合你的需要,你将需要定义一个定制的GCStrategy,并且可能的,一个执行降级的定制LLVM遍。何处开始定义一个定制GCStrategy最好的例子是查看其中一个内置策略。
你可能能够把这额外的代码组织为一个可载入插件库。如果你所需要是获取内置功能的一个不同的组合,可载入插件足矣,但如果你需要提供一个定制降级遍,你将需要构建LLVM的一个修补(patched)版本。如果你认为你需要一个修补版本,请咨询llvm-dev。我们可能有一个简单的扩展支持的方法,使它对你的用例生效,而无需一个定制的构建。
收集器要求
你应该能够利用任何现存的,包含下列元素的收集器库:
1. 导出你编译后代码可以调用的分配函数的一个内存分配器。
2. 栈映射的一个二进制格式。一个栈映射描述了在一个安全点处引用的位置,由精确收集器用来在机器栈的一个栈帧内识别引用。注意保守地扫描栈的收集器不需要这样的数据结构。
3. 在调用栈上发现函数,并对每个调用点枚举列出在栈映射中的引用的一个栈爬行者。
4. 在全局位置(即全局变量)中识别引用的一个机制。
5. 如果你的收集器要求它们,你收集器读写栅栏的一个LLVM IR实现。注意因为许多收集器完全不需要栅栏,LLVM缺省将这样的栅栏降级为普通的读写,除非你另有安排。
实现一个收集器插件
用户代码以gc函数属性,或等效地,以Function的setGC方法,指定要使用的GC代码生成。
要实现一个GC插件,从llvm::GCStrategy派生是必须的,它可以在几行样板代码(boilerplatecode)中完成。LLVM的基础设施提供对几个重要算法的访问。对于一个非争议的收集器,剩下的可能就是把LLVM计算的栈映射编译为汇编代码(使用运行时库所期望的二进制表示)。这可以在100行代码内完成。
这不是适合实现一个垃圾收集堆或垃圾收集器的地方。那些代码应该存在于语言的运行时库里。编译器插件负责生成符合库定义的二进制接口的代码,最重要的是栈映射。
要从llvm::GCStrategy派生,并注册到编译器:
//lib/MyGC/MyGC.cpp - Example LLVM GC plugin
#include"llvm/CodeGen/GCStrategy.h"
#include"llvm/CodeGen/GCMetadata.h"
#include"llvm/Support/Compiler.h"
using namespace llvm;
namespace {
class LLVM_LIBRARY_VISIBILITY MyGC : public GCStrategy {
public:
MyGC() {}
};
GCRegistry::Add<MyGC>
X("mygc", "My bespokegarbage collector.");
}
这个样板收集器不作任何事情。更具体地说:
· Llvm.gcread调用被相应的load指令替换。
· Llvm.gcwrite调用被相应的store指令替换。
· 代码中没有添加安全点、
· 栈映射被编译进可执行代码。
通过LLVM makefile,这个代码可以使用一个简单的makefile编译为一个插件:
#lib/MyGC/Makefile
LEVEL := ../..
LIBRARYNAME = MyGC
LOADABLE_MODULE = 1
include$(LEVEL)/Makefile.common
一旦该插件编译完成,使用它的代码可以使用llc -load=MyGC.so编译(虽然MyGC.so可能有其他平台特定的扩展):
$cat sample.ll
definevoid @f() gc "mygc" {
entry:
ret void
}
$llvm-as < sample.ll | llc -load=MyGC.so
将收集器插件静态链接入工具,比如语言特定的编译器前端,也是可能的。
可用特性的概览
GCStrategy提供了一组特性,从中插件可以进行有用的工作。其中某些是回调,某些是可以启用、禁止或定制的算法。下面的表格总结了支持的(及计划的)特性,把它们与通常要求它们的收集技术对应起来。
算法 | 完成 | 影子栈 | 引用计数 | 标记-清除 | 拷贝 | 增量 | 线程化 | 并发 |
栈映射 | ✔ | ✘ | ✘ | ✘ | ✘ | ✘ | ||
初始化根 | ✔ | ✘ | ✘ | ✘ | ✘ | ✘ | ✘ | ✘ |
派生指针 | NO | N* | N* | |||||
定制降级 | ||||||||
gcroot | ✔ | ✘ | ✘ | |||||
gcwrite | ✔ | ✘ | ✘ | ✘ | ||||
gcread | ✔ | ✘ | ||||||
安全点 | ||||||||
调用中 | ✔ | ✘ | ✘ | ✘ | ✘ | ✘ | ||
调用前 | ✔ | ✘ | ✘ | |||||
用于循环 | NO | N | N | |||||
before escape | ✔ | ✘ | ✘ | |||||
在安全点发布代码 | NO | N | N | |||||
输出 | ||||||||
assembly | ✔ | ✘ | ✘ | ✘ | ✘ | ✘ | ||
JIT | NO | ? | ? | ? | ? | ? | ||
obj | NO | ? | ? | ? | ? | ? | ||
生命期分析 | NO | ? | ? | ? | ? | ? | ||
寄存器映射 | NO | ? | ? | ? | ? | ? | ||
* 派生指针仅对拷贝收集造成危险。 | ||||||||
? 表示如果特性可用,就应该使用。 |
为了清楚起见,上面的收集技术被定义为:
影子栈(shadowstack)
小心维护栈根列表的修改者(mutator)。
引用计数(referencecounting)
对每个对象维护一个引用计数,在一个对象的计数降到0时释放它的修改者。
标记-清除(mark-sweep)
在堆耗尽时,收集器从根开始标记可到达的对象,然后在清扫阶段释放不可到达对象。
拷贝(copying)
随着可到达性分析的进行,收集器将对象从一个堆区域拷贝到另一个,在这个过程中紧缩它们。拷贝收集器使高效的“bumppointer”分配成为可能,并提高了引用的局部性。
增量(incremental)
(包括分代收集器)。增量收集器通常具有拷贝收集器的所有特性(不管成熟的堆是否正在紧缩),但带来的额外复杂性要求写栅栏。
线程化(threaded)
表示一个多线程化的修改者;收集器仍然必须在开始可到达性分析前停止修改者(停止所有处理,stopthe world)。停止一个多线程的修改者是一个复杂的问题。它通常在运行时中要求高度平台特定的代码,并在安全点产生仔细设计的机器代码。
并发(concurrent)
在这个技术里,修改者与收集器同时运行,目的是消除暂停时间。在一个协作收集器里,进一步辅助收集的修改者应该触发一次暂停,允许收集利用主机的多处理器(Ina cooperative collector, the mutator further aids with collectionshould a pause occur, allowing collection to take advantage of multiprocessorhosts)。线程化收集器的“停止所有处理”的问题通常仅以有限程度呈现。复杂的标记算法是必须的。可能需要读栅栏。
正如表格所示,LLVM的垃圾收集基础设施已经适用于很多种收集器,但目前没有扩展到多线程程序。这将在将来添加,因为对此感兴趣。
计算栈映射
LLVM自动地计算栈映射。GCStrategy最重要的一个特性是,将这个信息编译进以运行时库期望的二进制表示的可执行文件。
栈映射包含模块中每个函数里每个GC根的位置与身份。对每个根:
· RootNum:根的索引。
· StackOffset:对象相对于帧指针的偏移。
· RootMetadata:作为@llvm.gcroot固有函数%metadata参数传递的值。
同样,对作为整体的函数:
· getFrameSize():函数初始栈帧的整体大小,不包括任何动态分配。
· roots_size():函数中根的个数。
使用来自GCMetadataPrinter的GCFunctionMetadata::roots_begin() 与-end() 来访问栈映射。
for (iterator I = begin(), E = end(); I != E; ++I) {
GCFunctionInfo *FI = *I;
unsigned FrameSize = FI->getFrameSize();
size_t RootCount = FI->roots_size();
for (GCFunctionInfo::roots_iterator RI = FI->roots_begin(),
RE = FI->roots_end();
RI != RE; ++RI) {
int RootNum = RI->Num;
int RootStackOffset = RI->StackOffset;
Constant *RootMetadata = RI->Metadata;
}
}
如果llvm.gcroot固有函数在代码生成之前被一个定制降级遍消除了,LLVM将计算一个空的栈映射。这对实现引用计数或影子栈的收集器插件可能是有用的。
将根初始化为null:InitRoots
MyGC::MyGC() {
InitRoots = true;
}
在设置时,LLVM将自动地在函数入口将每个根初始化为null。这防止在GC的清除阶段访问未初始化指针,这几乎一定会导致崩溃。这初始化在定制降级前发生,因此两者可以同时使用。
因为LLVM还没计算生命期信息,没有区分未初始化栈根与初始化栈根。因此,这个特性应该为所有的GC插件使用。它缺省开启。
固有函数的定制降级:CustomRoots,CustomReadBarriers与CustomWriteBarriers
对于使用栅栏或不常见的栈根处理的GC,这些标记允许收集器执行LLVMIR的任意转换:
class MyGC : public GCStrategy {
public:
MyGC() {
CustomRoots = true;
CustomReadBarriers = true;
CustomWriteBarriers = true;
}
};
如果设置了任一标记,LLVM抑制对应固有函数的缺省降级。取而代之,你必须提供如预期降级这些固有函数的一个定制遍。如果你已经决定定制降级一个特定的固有函数,你的遍必须消除加入你GC函数里的对应固有函数的所有实例。这样一个遍的最好的例子是ShadowStackGC与其ShadowStackGCLowering遍。
目前没有方法注册这样一个定制降级遍,而无需构建LLVM的一个定制拷贝。
生成安全点:NeededSafePoints
LLVM可以计算四种安全点:
namespace GC {
/// PointKind - The type of a collector-safe point.
///
enum PointKind {
Loop, //<Instr is a loop (backwards branch).
Return, //<Instr is a return instruction.
PreCall, //< Instr is a call instruction.
PostCall //< Instr is the return address of a call.
};
}
收集器可以通过设置NeededSafePoints掩码请求这四种的任意组合:
MyGC::MyGC() {
NeededSafePoints = 1 << GC::Loop
| 1 << GC::Return
| 1 << GC::PreCall
| 1 << GC::PostCall;
}
然后,它可以使用以下例程来访问安全点。
for (iterator I = begin(), E = end(); I != E; ++I) {
GCFunctionInfo *MD = *I;
size_t PointCount = MD->size();
for (GCFunctionInfo::iterator PI = MD->begin(),
PE = MD->end(); PI != PE; ++PI) {
GC::PointKind PointKind = PI->Kind;
unsigned PointNum = PI->Num;
}
}
几乎每个收集器都要求PostCall安全点,因为这对应在调用子例程时函数被挂起的时刻。
多线程化程序通常要求Loop安全点来确保应用程序在有界的时间内到达一个安全点,即使它正在执行一个不包含函数调用的,长时间运行的循环。
多线程化收集器可能还要求Return及PreCall安全点,来实现使用自修改代码的“停止所有操作”技术,其中程序在没有到达一个安全点时,不退出函数(因为仅最顶层的函数被修补了)是重要的。
发布汇编代码:GCMetadataPrinter
LLVM允许插件打印在一个模块余下汇编代码前后的任意汇编代码。在该模块末尾,GC可以将LLVM的栈映射编译进汇编代码。(在开始,还没计算这个信息)。
因为AsmWriter与CodeGen是LLVM的独立组件,对打印汇编代码提供了一个独立的抽象基类与注册处,GCMetadaPrinter 与GCMetadataPrinterRegistry。如果GCStrategy设置了UsesMetadata,该ASMWriter将查找这样一个子类:
MyGC::MyGC() {
UsesMetadata = true;
}
这个分离允许Jit-only客户端变得更小。
注意LLVM目前没有类似的API来支持JIT中的代码生成,也不使用对象编写者。
// lib/MyGC/MyGCPrinter.cpp - Example LLVM GC printer
#include "llvm/CodeGen/GCMetadataPrinter.h"
#include "llvm/Support/Compiler.h"
using namespace llvm;
namespace {
class LLVM_LIBRARY_VISIBILITYMyGCPrinter : public GCMetadataPrinter {
public:
virtual voidbeginAssembly(AsmPrinter &AP);
virtual void finishAssembly(AsmPrinter &AP);
};
GCMetadataPrinterRegistry::Add<MyGCPrinter>
X("mygc", "My bespoke garbage collector.");
}
收集器应该使用AsmPrinter来输出可移植的汇编代码。收集器本身包含整个模块的栈映射,并可能使用自己的begin()与end()方法访问GCFunctionInfo。下面是一个真实的例子:
#include "llvm/CodeGen/AsmPrinter.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/DataLayout.h"
#include "llvm/Target/TargetAsmInfo.h"
#include "llvm/Target/TargetMachine.h"
void MyGCPrinter::beginAssembly(AsmPrinter&AP){
// Nothing to do.
}
void MyGCPrinter::finishAssembly(AsmPrinter&AP){
MCStreamer &OS = AP.OutStreamer;
unsigned IntPtrSize =AP.TM.getSubtargetImpl()->getDataLayout()->getPointerSize();
// Put this in the data section.
OS.SwitchSection(AP.getObjFileLowering().getDataSection());
// For each function...
for (iterator FI = begin(), FE = end(); FI != FE; ++FI) {
GCFunctionInfo &MD = **FI;
// A compact GC layout. Emit this data structure:
//
// struct {
// int32_t PointCount;
// void*SafePointAddress[PointCount];
// int32_t StackFrameSize;// in words
// int32_t StackArity;
// int32_t LiveCount;
// int32_tLiveOffsets[LiveCount];
// } __gcmap_<FUNCTIONNAME>;
// Align to address width.
AP.EmitAlignment(IntPtrSize == 4 ? 2 : 3);
// Emit PointCount.
OS.AddComment("safe point count");
AP.EmitInt32(MD.size());
// And each safe point...
for (GCFunctionInfo::iterator PI = MD.begin(),
PE =MD.end(); PI != PE; ++PI) {
// Emit the address of the safe point.
OS.AddComment("safe point address");
MCSymbol *Label = PI->Label;
AP.EmitLabelPlusOffset(Label/*Hi*/, 0/*Offset*/, 4/*Size*/);
}
// Stack information never change in safe points! Only printinfo from the
// first call-site.
GCFunctionInfo::iterator PI = MD.begin();
// Emit the stack frame size.
OS.AddComment("stack frame size (in words)");
AP.EmitInt32(MD.getFrameSize() / IntPtrSize);
// Emit stack arity, i.e. the number of stacked arguments.
unsigned RegisteredArgs = IntPtrSize == 4 ? 5 : 6;
unsigned StackArity =MD.getFunction().arg_size() > RegisteredArgs ?
MD.getFunction().arg_size() - RegisteredArgs : 0;
OS.AddComment("stack arity");
AP.EmitInt32(StackArity);
// Emit the number of live roots in the function.
OS.AddComment("live root count");
AP.EmitInt32(MD.live_size(PI));
// And for each live root...
for (GCFunctionInfo::live_iterator LI = MD.live_begin(PI),
LE = MD.live_end(PI);
LI != LE; ++LI) {
// Emit live root's offset within the stack frame.
OS.AddComment("stack index (offset / wordsize)");
AP.EmitInt32(LI->StackOffset);
}
}
}
参考文献
[Appel89]Runtime Tags Aren’t Necessary. Andrew W. Appel. Lisp and Symbolic Computation19(7):703-705, July 1989.
[Goldberg91]Tag-free garbage collection for strongly typed programming languages. BenjaminGoldberg. ACM SIGPLAN PLDI‘91.
[Tolmach94]Tag-free garbage collection using explicit type parameters. Andrew Tolmach.Proceedings of the 1994 ACM conference on LISP and functional programming.
[Henderson2002] Accurate Garbage Collection in an Uncooperative Environment