lua5.3 gc写屏障分析(3)
在 GC 的扫描过程中,由于分步执行,会出现一些已经被扫描过了,被标记为 黑色 black 的对象,再引用到其他新的对象。比如,表 table 就是最容易出现这种情况的。那这种 black 对象再引用其他白色对象,lua是怎么处理的呢?
lua gc 使用两种方式解决,一种是 barrier back,另一种是 barrier forward
- barrier back(向后退一步):将一个 black 状态的对象改成 gray,并加入到 g->grayagain 链表中,相对于后退一步,待原子阶段,重新 mark。
- barrier forward(向前一步):将一个 white 对象改成 gray, 并加入到 g->gray 链表中,无需通过走正常流程 singlestep() 来去驱动 mark,相对于前进了一步。
如下图:

barrier back 主要是针对频繁修改引用关系的对象,比如,table 表,如果对 table 表引用的对象,做向前操作,那么意味中,一个表只要要新增对象,就需要把这个新对象标记为灰色并加入 gray 链表等待扫描。不是说不可以,我个人觉得还是因为,如果一个 table 的同一个 key 频繁发生改变,指向不同的新的对象,那么之前指向的旧对象,已经被 mark 为灰色,最终也会转成黑色 black,导致没法被回收,只能待到下一轮 gc 才回收,这样就会出现回收不及时了,而且也加大了标记阶段的时间。所以,一旦被判断为频繁改变的黑色 black 对象,就会被退回变成灰色,并加入到 grayagain 链表中,注意,这里用的链表不是 gray 链表(放到 gray 链表,还是会出现反复标记的情况,black -> gray -> black -> gray)。而放到 grayagain 链表中,只会改变一次颜色,待到原子阶段,只需再扫描一次就可以了。
#define luaC_barrierback(L,p,v) ( \
(iscollectable(v) && isblack(p) && iswhite(gcvalue(v))) ? \
luaC_barrierback_(L,p) : cast_void(0))
/*
** barrier that moves collector backward, that is, mark the black object
** pointing to a white object as gray again.
*/
void luaC_barrierback_ (lua_State *L, Table *t) {
global_State *g = G(L);
lua_assert(isblack(t) && !isdead(g, t));
black2gray(t); /* make table gray (again) */
linkgclist(t, g->grayagain);
}
//lapi.c 文件
LUA_API void lua_rawseti (lua_State *L, int idx, lua_Integer n) {
...
luaC_barrierback(L, hvalue(o), L->top-1);
...
}
LUA_API void lua_rawsetp (lua_State *L, int idx, const void *p) {
...
luaC_barrierback(L, hvalue(o), L->top - 1);
...
}
// lvm.c 文件
void luaV_finishset (lua_State *L, const TValue *t, TValue *key,
StkId val, const TValue *slot) {
...
luaC_barrierback(L, h, val);
...
}
我们可以从 lapi.c 和 lvm.c 文件中看到,有很多设置表 key-value 的接口,都加了luaC_barrierback()。因为我们平时对数据操作最多的也是表,所以,如果处在 gc 标记阶段中,是有必要做 barrier back 检查的。同时,大家也从上面的宏定义luaC_barrierback()中注意到,它是没有判断当前 gc 处于哪个阶段的,也就是说,无论 gc 处于哪个阶段,执行 luaC_barrierback()都不会有负作用影响。那为啥不加判断呢,我猜测可能是为了性能考虑,因为这些设置值的 API 有可能是程序运作中,执行次数最多的接口,所以,执行越少的代码程序越快,而且把 luaC_barrierback() 设计成宏而不是函数,应该也可以说明这一点。
在清除阶段,如果对象是黑色,执行了 luaC_barrierback()会使得黑色对象退回灰色,并且被挂在 g->grayagain 链表上,但这也不会有什么影响,能发现对象此时还是黑色的,说明这个对象还没经过 sweeplist 处理,待到 sweeplist 处理时,就会把这个对象从灰色重置为当前白。
这时就会存在一种情况,一个对象被挂在 g->grayagain 链表上,但是它是白色。这也是 lua 的巧妙之处,虽然一个白色对象挂在 g->grayagain 上, 但是它不会有副作用。因为在 restartcollection 函数中的 g->gray = g->grayagain = NULL语句会将整个 grayagain 链表清空。同时虽然白色对象的 gclist 字段依然有无效值,但是在下次插入 gray 或 grayagain 之前,会对此对象的 gclist字段重新赋值,因此也不会有任何副作用。
barrier forward 的作用和 barrier back 相反,主要是针对那些引用关系不会经常发生改变的对象,比如,设置元表,Proto 函数原型引用到的字符串等。像我们平时写面向对象,就经常会用到设置元表,但一般来说,只会设置一次,不会频繁变更元表,还有 Proto 函数原型这些,基本上引用到的其他对象,是很少再去改变的。
// lgc.h 文件
#define luaC_barrier(L,p,v) ( \
(iscollectable(v) && isblack(p) && iswhite(gcvalue(v))) ? \
luaC_barrier_(L,obj2gco(p),gcvalue(v)) : cast_void(0))
#define luaC_objbarrier(L,p,o) ( \
(isblack(p) && iswhite(o)) ? \
luaC_barrier_(L,obj2gco(p),obj2gco(o)) : cast_void(0))
#define keepinvariant(g) ((g)->gcstate <= GCSatomic)
/*
** barrier that moves collector forward, that is, mark the white object
** being pointed by a black object. (If in sweep phase, clear the black
** object to white [sweep it] to avoid other barrier calls for this
** same object.)
*/
void luaC_barrier_ (lua_State *L, GCObject *o, GCObject *v) {
global_State *g = G(L);
lua_assert(isblack(o) && iswhite(v) && !isdead(g, v) && !isdead(g, o));
if (keepinvariant(g)) /* must keep invariant? */
reallymarkobject(g, v); /* restore invariant */
else { /* sweep phase */
lua_assert(issweepphase(g));
makewhite(g, o); /* mark main obj. as white to avoid other barriers */
}
}
// lapi.c 文件
LUA_API int lua_setmetatable (lua_State *L, int objindex) {
...
switch (ttnov(obj)) {
case LUA_TTABLE: {
hvalue(obj)->metatable = mt;
if (mt) {
luaC_objbarrier(L, gcvalue(obj), mt);
...
}
// lparser.c 文件
static int registerlocalvar (LexState *ls, TString *varname) {
FuncState *fs = ls->fs;
Proto *f = fs->f;
...
luaC_objbarrier(ls->L, f, varname);
...
}
lgc.h 提供了两个宏,一个是 luaC_barrier(),另一个是 luaC_objbarrier() ,它们的主要区别是,一个针对值是 TValue 的,另一个是针对 GCObject 的,比如在栈中,我们的对象是 TValue 数据类型,用 luaC_barrier() 合适。如果我们已经获取到 GCObject 对象了,比如像设置元表的接口那里,用 luaC_objbarrier()合适。
在 luaC_barrier_() 实现中,会判断当前是否处于扫描阶段((g)->gcstate <= GCSatomic) ,如果是的话,被引用的对象就向前一步,mark gray,如果不是处于扫描阶段(有可能是清除阶段,也有可能是 gc 暂停阶段)时,会把引用对象 o 置为白色,这个白色是安全的,之前我们说过,标记阶段的白色是要在清除阶段回收的,而其他阶段的白色则留到下一轮 gc 回收。把引用对象重新标记回白色,主要目的也是为了防止多次对其进行 barrier 操作,减少不必要开销,如果再对 o 赋值或者其他操作,也不会再产生 luaC_barrier 调用。

浙公网安备 33010602011771号