Skip to content

Commit b19b928

Browse files
committed
feat: more escapable types && fix docs
1 parent 2b0ac64 commit b19b928

File tree

5 files changed

+30
-58
lines changed

5 files changed

+30
-58
lines changed

book/src/systemlib/gc.md

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,20 @@
11
# GC
22

3-
> !WIP:此页面描述的功能仍然在实验性阶段,可能有些细节没有实现完毕。
4-
5-
63
pivot-lang 是一门使用gc进行内存管理的语言。
74

85
pivot-lang 的gc目前是使用rust写的,采用一种叫做`immix`[\[1\]]的mark region算法。
96

107
在以前的版本中,我们使用的是我们现在叫做`simple gc`的垃圾回收算法,他是一个简单的mark-sweep算法。
11-
它由于性能问题和不支持多线程等原因最终被`immix`取代。不过我们仍然保留了它的代码,并且设置了一个编译开关(_feature:simple_gc_),可以在编译时手动选择使用该gc算法。
8+
它由于性能问题和不支持多线程等原因最终被`immix`取代。
129

1310

1411
[\[1\]]: https://www.cs.utexas.edu/users/speedway/DaCapo/papers/immix-pldi-2008.pdf
1512

16-
## Simple GC
17-
18-
simple gc就和他的名字一样,[代码](https://github.com/Pivot-Studio/pivot-lang/blob/master/vm/src/gc/_simple_gc.rs)十分简单(算法实现大概100行)。它没有自己的allocator,直接
19-
使用C中的`malloc``free`进行内存分配和释放。
20-
21-
simple gc是一个[保守gc算法(Conservative Garbage Collection)](https://www.cs.cornell.edu/courses/cs6120/2020fa/blog/modern-gc/),它没有能力精确的分辨出哪些内存是指针,哪些不是。因此它会试图将所有的内存都当做指针来处理。
22-
23-
目前simple gc不建议在生产环境中使用,其代码保留下来是为了方便我们在未来的版本中进行性能对比,以及为未来用于教育目的做准备。
24-
25-
2613
## Immix GC
2714

2815
immix gc是一种mark region算法,它是一个精确的gc算法。但是请注意,它的精确建立在使用它
2916
的项目提供的特殊支持之上。可以认为目前我们的immix gc实现 __是为pivot-lang量身定制__ 的。pl编译器为了和我们的immix gc配合,会在编译时专门生成一些额外的代码,如果缺少这些代码,immix gc将无法正常工作。
3017
所以虽然理论上我们可以将我们的immix gc用到其他项目中,这么做的效益很可能并不是很高--
3118
缺少编译器的支持,使用者将需要手动添加那些额外的代码。
3219

33-
immix gc的实现代码在[这里](https://github.com/Pivot-Studio/pivot-lang/blob/master/immix)。它是天生支持多线程使用的,但是我们的pivot-lang目前还不支持多线程。
34-
35-
36-
## Benchmark
37-
38-
我们对两种gc算法进行了一些基准测试,事实证明immix gc的回收性能要比simple gc __快近20倍__。如果你对这些测试感兴趣,可以在项目根目录运行`make bench`查看immix的benchmark,或者运行`make bench-simple-gc`查看simple gc的benchmark。
39-
40-
下方分别是simple gc和immix gc的benchmark结果,测试于2023年1月,commit 62b5c52c01e8133f5300e33a0131a50ba0c8d0de
41-
42-
![](2023-01-24-23-23-55.png)
43-
44-
![](2023-01-24-23-25-06.png)
20+
immix gc的实现代码在[这里](https://github.com/Chronostasys/immix)

book/src/systemlib/immix.md

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,34 +6,32 @@
66
此页面仍在编写中,内容可能有疏漏
77
```
88

9-
109
## Table of Contents
1110

1211
<!-- toc -->
1312

1413
## Overview
1514

16-
此gc是我们基于[immix gc论文](https://www.cs.utexas.edu/users/speedway/DaCapo/papers/immix-pldi-2008.pdf)实现的,
17-
大部分的实现细节都与论文一致,对于一些论文没提到的细节我们自行进行了实现,参考了很多别的gc项目。该gc是一个支持多线程使用的、
18-
基于shadow stack的,精确mark-region 非并发(Concurrency) 并行(Parallelism) gc。
19-
15+
此gc是我们基于[immix GC论文](https://www.cs.utexas.edu/users/speedway/DaCapo/papers/immix-pldi-2008.pdf)实现的,
16+
大部分的实现细节都与论文一致,对于一些论文没提到的细节我们自行进行了实现,参考了很多别的GC项目。该GC是一个支持多线程使用的、
17+
基于StackMap的,半精确mark-region 非并发(Concurrency) 并行(Parallelism) gc。
2018

2119
```admonish tip title="gc的并发(Concurrency)与并行(Parallelism)"
2220
gc中并发和并行是两个不同的术语,并发gc指的是能够在应用不暂停的基础上进行回收的gc,
2321
而并行gc指的是gc在回收的时候能够使用多个线程同时进行工作。一个gc可以既是并行的也是并发的,
2422
我们的immix gc目前只具备并行能力
2523
```
24+
2625
这里有一些创建该gc过程中我们主要参考的资料,列表如下:
2726

2827
- [immix gc论文](https://www.cs.utexas.edu/users/speedway/DaCapo/papers/immix-pldi-2008.pdf)
2928
- [playxe 的 immixcons(immix gc的一个rust实现,回收存在bug)-- 很多底层内存相关代码是参考该gc完成的,还有在函数头中加入自定义遍历函数的做法](https://github.com/playXE/libimmixcons)
3029
- [给scala-native使用的一个immix gc的C实现](https://github.com/scala-native/immix)
3130
- [康奈尔大学CS6120课程关于immix gc的博客,可以帮助快速理解论文的基本思路](https://www.cs.cornell.edu/courses/cs6120/2019fa/blog/immix/)
3231

33-
3432
## General Description
3533

36-
本gc是为pl __定制的__,虽然理论上能被其他项目使用,但是对外部项目的支持**并不是主要目标**
34+
本gc是为pl __定制的__,虽然理论上能被其他项目使用,但是对外部项目的支持 __并不是主要目标__
3735

3836
pl的组件包含一个全局的`GlobalAllocator`,然后每个`mutator`线程会包含一个独属于该线程的`Collector`,每个`Collector`中包含一个
3937
`ThreadLocalAllocator`。在线程使用gc相关功能的时候,该线程对应的`Collector`会自动被创建,直到线程结束或者
@@ -114,7 +112,6 @@ graph LR;
114112
Immix GC由于分配以line为单位,在内存利用率上稍有不足,但是其算法保证了极其优秀的内存局部性,配合TLA、GA的设计也大大
115113
减少了线程间的竞争。
116114

117-
118115
## Allocation
119116

120117
### Global Allocator
@@ -130,7 +127,8 @@ GA同时维护一个`free`动态数组,用于存储已经被回收的block,
130127
```admonish info
131128
潜在的优化点:使用bitmap来记录block的使用情况,这样可以减少动态数组的开销
132129
```
133-
```admonish warning
130+
131+
```admonish warning
134132
如果GA在分配新block的时候,发现`current`指针已经超出了初始申请的内存空间,会导致程序panic。这个行为应该在未来被改善
135133
```
136134

@@ -149,6 +147,7 @@ TLA是线程本地的,每个线程都会有一个TLA,负责分配line,和
149147
所有完全被占用的block会被加入到`unavailable`数组中,不会被重复利用。
150148

151149
TLA的小对象分配策略如下:
150+
152151
<center>
153152

154153
```mermaid
@@ -183,16 +182,18 @@ graph TD;
183182

184183
## Mark
185184

186-
Mark阶段的主要工作是标记所有被使用的line和block,以便在后续的sweep阶段进行回收。我们的mark算法是**精确**
185+
Mark阶段的主要工作是标记所有被使用的line和block,以便在后续的sweep阶段进行回收。我们的mark算法是__精确__
187186
这点对evacuation算法的实现至关重要。
188187

189188
精确GC有两个要求:
189+
190190
- root的精确定位
191191
- 对象的精确遍历
192192

193-
我们的精确root定位是基于**Stack Map**,这部分细节过于复杂,将在[单独的文档](stackmap.md)中介绍。
193+
我们的精确root定位是基于__Stack Map__,这部分细节过于复杂,将在[单独的文档](stackmap.md)中介绍。
194194

195195
对象的精确遍历是通过编译器支持实现的,plimmix将所有heap对象分类为以下4种:
196+
196197
- Atomic Object:原子对象,不包含指针的对象,如整数、浮点数、字符串等
197198
- Pointer Object:指针对象,该对象本身是一个指针
198199
- Complex Object:复杂对象,该对象可能包含指针
@@ -204,11 +205,13 @@ Mark阶段的主要工作是标记所有被使用的line和block,以便在后
204205

205206
对于Complex Object,编译器需要在对象开始位置增加一个`vtable`字段,该字段的值指向该类型的遍历函数。此遍历函数由编译器生成,
206207
其签名为:
208+
207209
```rust,ignore
208210
pub type VisitFunc = unsafe fn(&Collector, *mut u8);
209211
// vtable的签名,第一个函数是mark_ptr,第二个函数是mark_complex,第三个函数是mark_trait
210212
pub type VtableFunc = fn(*mut u8, &Collector, VisitFunc, VisitFunc, VisitFunc);
211213
```
214+
212215
在标记的时候,我们会调用对象的vtable对他进行遍历
213216

214217
对于Trait Object,我们需要遍历他指向实际值的指针
@@ -253,13 +256,16 @@ graph LR;
253256
```
254257

255258
对于`ComplexObject1`,他的vtable函数逻辑如下:
259+
256260
```rust,ignore
257261
fn vtable_complex_obj1(&self, mark_ptr: VisitFunc, mark_complex: VisitFunc, mark_trait: VisitFunc){
258262
mark_ptr(self.PointerField)
259263
mark_complex(self.ComplexField)
260264
}
261265
```
266+
262267
而对于`ComplexField`,他的vtable函数逻辑如下:
268+
263269
```rust,ignore
264270
fn vtable_complex_field(&self, mark_ptr: VisitFunc, mark_complex: VisitFunc, mark_trait: VisitFunc){
265271
mark_ptr(self.PointerField)
@@ -279,10 +285,10 @@ mark queue为空,则标记过程结束。
279285
尽管这的确可以看作是一个递归的过程,但是此过程一定不能使用递归的方式实现,因为递归的方式在复杂程序中可能会导致栈溢出。
280286
```
281287

282-
283288
## Sweep
284289

285290
Sweep阶段的主要工作是:
291+
286292
- 回收所有未被标记的block
287293
- 修正所有line的header
288294
- 计算evacuation需要的一些信息
@@ -303,6 +309,7 @@ Sweep阶段的主要工作是:
303309
## Evacuation
304310

305311
每次回收开始之前,我们会先判断是否需要进行反碎片化,目前的策略是只要recycle block>1就进行反碎片化。
312+
306313
```admonish info
307314
优化点:如果处于内存用尽的紧急情况也应当进行evacuation,且threshold应当设置的更低
308315
```
@@ -377,7 +384,6 @@ graph TD;
377384

378385
每次驱逐是以分配的对象为单位,如果一个block被标记为待evacuate,那么在驱逐过程中,该block中的所有对象都一定会被驱逐。
379386

380-
381387
请注意,一部分其他gc的驱逐算法中的自愈需要读写屏障的参与,immix不需要。这带来了较大的mutator性能提升。
382388

383389
```admonish warning
@@ -415,13 +421,11 @@ fn add_sub_ndoe(root: *mut Node) {
415421
这导致gc无法在回收过程中对其进行修正,就会进一步导致`root.next`指向的地址不正确,从而导致程序出错。
416422
```
417423

418-
419424
```admonish tip title="多线程情况下驱逐算法的安全性"
420425
在多线程情况下,是存在两个线程同时驱逐一个对象的可能的,在这种情况下一些同步操作必不可少,但是并不需要加锁。
421426
我们通过一个cas操作来保证只有一个线程能够成功驱逐该对象。
422427
```
423428

424-
425429
## 性能
426430

427431
我们与bdwgc进行了很多比较,数据证明在大多数情况下,我们的分配算法略慢于bdwgc,与malloc速度相当,但是在回收的时候,我们的回收速度要快于bdwgc。
@@ -433,9 +437,9 @@ fn add_sub_ndoe(root: *mut Node) {
433437

434438
你可以从[这里](https://github.com/Chronostasys/bdwgcvsimmix-bench)下载测试代码,在你的机器上运行并进行比较。这里我提供一组笔者机器上的测试数据截图
435439

436-
![](immix.png)
440+
![immix](immix.png)
437441

438-
![](bdw.png)
442+
![bdw](bdw.png)
439443

440444
测试环境为MacBook Pro (16-inch, 2021) Apple M1 Pro 16 GB,可以看出immix在此环境中已经具有近4倍的性能优势。
441445

book/src/systemlib/stackmap.md

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ llvm提供了一系列的gc相关api,首先是gc策略,我们可以给每个
3232
两个C文件一个定义了我们的GC策略,一个定义了我们的stackmap格式和生成方式。
3333

3434
我们的stackmap格式如下:
35-
```
35+
36+
```Rust
3637
Header {
3738
i64 : Stack Map Version (current version is 1)
3839
i32 : function 数量
@@ -117,22 +118,13 @@ immix gc提供了`gc_init`函数,该函数接受一个stackmap指针,会加
117118

118119
### 基于stackmap的精确root定位实现
119120

120-
为了遍历函数调用栈,我们使用`backtrace.rs`包,该包封装了一些平台相关的函数调用栈遍历的实现。
121-
```admonish warning
122-
栈爬取的实现不同平台差异巨大,很容易出现bug。目前我们发现在mac aarch64上如果使用lld进行链接会导致该backtrace包出现
123-
segment fault,这个问题目前使用ld替代lld进行规避。
124-
```
125-
126-
遍历的时候通过`backtrace.rs`拿到当前ip寄存器的值,然后去我们构建的stackmap中查找到当前函数栈的root进行遍历,遍历完成后继续向上层函数栈遍历。
127-
注意这里遍历的起点不在mutator代码中,而在gc代码中,所以遍历的开头和结尾查不到对应记录是完全正常的。
121+
为了遍历函数调用栈,在老版本中我们使用`backtrace.rs`包,该包封装了一些平台相关的函数调用栈遍历的实现。
128122

129-
```admonish
130-
潜在优化点:其实从gc的函数到目标语言最底层函数的调用栈层数在运行时是固定的,所以这里其实可以优化,跳过前几个栈帧,直接从目标语言最底层函数开始遍历。
131-
```
123+
在新版本中,由于`libbacktrace`中使用了全居锁,性能不好,我们决定手动进行栈遍历。为了完成这一点,我们需要
124+
在mutator调用可能触发GC的runtime函数时,传入当前的`sp`寄存器值,之后利用StackMap中记录的各个函数栈的大小来一步一步往上爬取。
132125

133126
## 参考资料
134127

135128
1. [llvm stackmap 文档](https://llvm.org/docs/StackMaps.html)
136129
2. [llvm gc 文档](https://llvm.org/docs/GarbageCollection.html)
137130
3. [读取llvm默认生成的stackmap例子](https://github.com/KavinduZoysa/test-GCs)
138-

src/ast/builder/llvmbuilder.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -446,7 +446,7 @@ impl<'a, 'ctx> LLVMBuilder<'a, 'ctx> {
446446
&format!("heapptr_{}", name),
447447
)
448448
.unwrap();
449-
if tp.is_atomic() || !matches!(tp, PLType::Arr(_)) {
449+
if !matches!(tp, PLType::Arr(_)) {
450450
let pos = match declare {
451451
Some(Pos {
452452
line: 0,

0 commit comments

Comments
 (0)