6
6
此页面仍在编写中,内容可能有疏漏
7
7
```
8
8
9
-
10
9
## Table of Contents
11
10
12
11
<!-- toc -->
13
12
14
13
## Overview
15
14
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。
20
18
21
19
``` admonish tip title="gc的并发(Concurrency)与并行(Parallelism)"
22
20
gc中并发和并行是两个不同的术语,并发gc指的是能够在应用不暂停的基础上进行回收的gc,
23
21
而并行gc指的是gc在回收的时候能够使用多个线程同时进行工作。一个gc可以既是并行的也是并发的,
24
22
我们的immix gc目前只具备并行能力
25
23
```
24
+
26
25
这里有一些创建该gc过程中我们主要参考的资料,列表如下:
27
26
28
27
- [ immix gc论文] ( https://www.cs.utexas.edu/users/speedway/DaCapo/papers/immix-pldi-2008.pdf )
29
28
- [ playxe 的 immixcons(immix gc的一个rust实现,回收存在bug)-- 很多底层内存相关代码是参考该gc完成的,还有在函数头中加入自定义遍历函数的做法] ( https://github.com/playXE/libimmixcons )
30
29
- [ 给scala-native使用的一个immix gc的C实现] ( https://github.com/scala-native/immix )
31
30
- [ 康奈尔大学CS6120课程关于immix gc的博客,可以帮助快速理解论文的基本思路] ( https://www.cs.cornell.edu/courses/cs6120/2019fa/blog/immix/ )
32
31
33
-
34
32
## General Description
35
33
36
- 本gc是为pl __ 定制的__ ,虽然理论上能被其他项目使用,但是对外部项目的支持** 并不是主要目标 **
34
+ 本gc是为pl __ 定制的__ ,虽然理论上能被其他项目使用,但是对外部项目的支持 __ 并不是主要目标 __
37
35
38
36
pl的组件包含一个全局的` GlobalAllocator ` ,然后每个` mutator ` 线程会包含一个独属于该线程的` Collector ` ,每个` Collector ` 中包含一个
39
37
` ThreadLocalAllocator ` 。在线程使用gc相关功能的时候,该线程对应的` Collector ` 会自动被创建,直到线程结束或者
@@ -114,7 +112,6 @@ graph LR;
114
112
Immix GC由于分配以line为单位,在内存利用率上稍有不足,但是其算法保证了极其优秀的内存局部性,配合TLA、GA的设计也大大
115
113
减少了线程间的竞争。
116
114
117
-
118
115
## Allocation
119
116
120
117
### Global Allocator
@@ -130,7 +127,8 @@ GA同时维护一个`free`动态数组,用于存储已经被回收的block,
130
127
``` admonish info
131
128
潜在的优化点:使用bitmap来记录block的使用情况,这样可以减少动态数组的开销
132
129
```
133
- ``` admonish warning
130
+
131
+ ``` admonish warning
134
132
如果GA在分配新block的时候,发现`current`指针已经超出了初始申请的内存空间,会导致程序panic。这个行为应该在未来被改善
135
133
```
136
134
@@ -149,6 +147,7 @@ TLA是线程本地的,每个线程都会有一个TLA,负责分配line,和
149
147
所有完全被占用的block会被加入到` unavailable ` 数组中,不会被重复利用。
150
148
151
149
TLA的小对象分配策略如下:
150
+
152
151
<center >
153
152
154
153
``` mermaid
@@ -183,16 +182,18 @@ graph TD;
183
182
184
183
## Mark
185
184
186
- Mark阶段的主要工作是标记所有被使用的line和block,以便在后续的sweep阶段进行回收。我们的mark算法是 ** 精确 ** 的 ,
185
+ Mark阶段的主要工作是标记所有被使用的line和block,以便在后续的sweep阶段进行回收。我们的mark算法是 __ 精确 __ 的 ,
187
186
这点对evacuation算法的实现至关重要。
188
187
189
188
精确GC有两个要求:
189
+
190
190
- root的精确定位
191
191
- 对象的精确遍历
192
192
193
- 我们的精确root定位是基于 ** Stack Map ** 的 ,这部分细节过于复杂,将在[ 单独的文档] ( stackmap.md ) 中介绍。
193
+ 我们的精确root定位是基于 __ Stack Map __ 的 ,这部分细节过于复杂,将在[ 单独的文档] ( stackmap.md ) 中介绍。
194
194
195
195
对象的精确遍历是通过编译器支持实现的,plimmix将所有heap对象分类为以下4种:
196
+
196
197
- Atomic Object:原子对象,不包含指针的对象,如整数、浮点数、字符串等
197
198
- Pointer Object:指针对象,该对象本身是一个指针
198
199
- Complex Object:复杂对象,该对象可能包含指针
@@ -204,11 +205,13 @@ Mark阶段的主要工作是标记所有被使用的line和block,以便在后
204
205
205
206
对于Complex Object,编译器需要在对象开始位置增加一个` vtable ` 字段,该字段的值指向该类型的遍历函数。此遍历函数由编译器生成,
206
207
其签名为:
208
+
207
209
``` rust,ignore
208
210
pub type VisitFunc = unsafe fn(&Collector, *mut u8);
209
211
// vtable的签名,第一个函数是mark_ptr,第二个函数是mark_complex,第三个函数是mark_trait
210
212
pub type VtableFunc = fn(*mut u8, &Collector, VisitFunc, VisitFunc, VisitFunc);
211
213
```
214
+
212
215
在标记的时候,我们会调用对象的vtable对他进行遍历
213
216
214
217
对于Trait Object,我们需要遍历他指向实际值的指针
@@ -253,13 +256,16 @@ graph LR;
253
256
```
254
257
255
258
对于` ComplexObject1 ` ,他的vtable函数逻辑如下:
259
+
256
260
``` rust,ignore
257
261
fn vtable_complex_obj1(&self, mark_ptr: VisitFunc, mark_complex: VisitFunc, mark_trait: VisitFunc){
258
262
mark_ptr(self.PointerField)
259
263
mark_complex(self.ComplexField)
260
264
}
261
265
```
266
+
262
267
而对于` ComplexField ` ,他的vtable函数逻辑如下:
268
+
263
269
``` rust,ignore
264
270
fn vtable_complex_field(&self, mark_ptr: VisitFunc, mark_complex: VisitFunc, mark_trait: VisitFunc){
265
271
mark_ptr(self.PointerField)
@@ -279,10 +285,10 @@ mark queue为空,则标记过程结束。
279
285
尽管这的确可以看作是一个递归的过程,但是此过程一定不能使用递归的方式实现,因为递归的方式在复杂程序中可能会导致栈溢出。
280
286
```
281
287
282
-
283
288
## Sweep
284
289
285
290
Sweep阶段的主要工作是:
291
+
286
292
- 回收所有未被标记的block
287
293
- 修正所有line的header
288
294
- 计算evacuation需要的一些信息
@@ -303,6 +309,7 @@ Sweep阶段的主要工作是:
303
309
## Evacuation
304
310
305
311
每次回收开始之前,我们会先判断是否需要进行反碎片化,目前的策略是只要recycle block>1就进行反碎片化。
312
+
306
313
``` admonish info
307
314
优化点:如果处于内存用尽的紧急情况也应当进行evacuation,且threshold应当设置的更低
308
315
```
@@ -377,7 +384,6 @@ graph TD;
377
384
378
385
每次驱逐是以分配的对象为单位,如果一个block被标记为待evacuate,那么在驱逐过程中,该block中的所有对象都一定会被驱逐。
379
386
380
-
381
387
请注意,一部分其他gc的驱逐算法中的自愈需要读写屏障的参与,immix不需要。这带来了较大的mutator性能提升。
382
388
383
389
``` admonish warning
@@ -415,13 +421,11 @@ fn add_sub_ndoe(root: *mut Node) {
415
421
这导致gc无法在回收过程中对其进行修正,就会进一步导致`root.next`指向的地址不正确,从而导致程序出错。
416
422
```
417
423
418
-
419
424
``` admonish tip title="多线程情况下驱逐算法的安全性"
420
425
在多线程情况下,是存在两个线程同时驱逐一个对象的可能的,在这种情况下一些同步操作必不可少,但是并不需要加锁。
421
426
我们通过一个cas操作来保证只有一个线程能够成功驱逐该对象。
422
427
```
423
428
424
-
425
429
## 性能
426
430
427
431
我们与bdwgc进行了很多比较,数据证明在大多数情况下,我们的分配算法略慢于bdwgc,与malloc速度相当,但是在回收的时候,我们的回收速度要快于bdwgc。
@@ -433,9 +437,9 @@ fn add_sub_ndoe(root: *mut Node) {
433
437
434
438
你可以从[ 这里] ( https://github.com/Chronostasys/bdwgcvsimmix-bench ) 下载测试代码,在你的机器上运行并进行比较。这里我提供一组笔者机器上的测试数据截图
435
439
436
- ![ ] ( immix.png )
440
+ ![ immix ] ( immix.png )
437
441
438
- ![ ] ( bdw.png )
442
+ ![ bdw ] ( bdw.png )
439
443
440
444
测试环境为MacBook Pro (16-inch, 2021) Apple M1 Pro 16 GB,可以看出immix在此环境中已经具有近4倍的性能优势。
441
445
0 commit comments