今天来说说 write barrier 。

在 GC 的扫描过程中,由于分步执行,难免会出现少描了一半时,那些已经被置黑的对象又被修改,需要重新标记的情况。这就需要在改写对象时,建立 write barrier 。在扫描过程中触发 write barrier 的操作影响的对象被正确染色,或是把需要再染色的对象记录下来,留到 mark 的最后阶段 atomic 完成。

和 barrier 相关的 API 有四个,定义在 lgc.h 86 行:

#define luaC_barrier(L,p,v) { if (valiswhite(v) && isblack(obj2gco(p)))  \
luaC_barrierf(L,obj2gco(p),gcvalue(v)); }
#define luaC_barriert(L,t,v) { if (valiswhite(v) && isblack(obj2gco(t)))  \
luaC_barrierback(L,t); }
#define luaC_objbarrier(L,p,o)  \
{ if (iswhite(obj2gco(o)) && isblack(obj2gco(p))) \
luaC_barrierf(L,obj2gco(p),obj2gco(o)); }
#define luaC_objbarriert(L,t,o)  \
{ if (iswhite(obj2gco(o)) && isblack(obj2gco(t))) luaC_barrierback(L,t); }

luaC_barrierluaC_objbarrier 功能上相同,只不过前者是针对 TValue 的,后者则针对 GCObject 。它用于把 v 向 p 关联时,当 v 为白色且 p 为黑色时,调用 luaC_barrierf

luaC_barriertluaC_objbarriert 则是用于将 v 关联到 t 时,调用 luaC_barrierback 。当然前提条件也是 v 为白色,且 t 为黑色。

为何 table 要被特殊对待?

因为 table 的引用关系是 1 对 N ,往往修改同一个 table 是很频繁的。而其它对象之间则是有限的联系。分开处理可以减少 barrier 本身的开销。

luaC_barrierf 用于把新建立联系的对象立刻标记。它的实现在 lgc.c 的 663 行:

void luaC_barrierf (lua_State *L, GCObject *o, GCObject *v) {global_State *g = G(L);
lua_assert(isblack(o) && iswhite(v) && !isdead(g, v) && !isdead(g, o));
lua_assert(g->gcstate != GCSfinalize && g->gcstate != GCSpause);
lua_assert(ttype(&o->gch) != LUA_TTABLE);
/* must keep invariant? */
if (g->gcstate == GCSpropagate)
reallymarkobject(g, v);  /* restore invariant */
else  /* don't mind */
makewhite(g, o);  /* mark as white just to avoid other barriers */
}

简而言之,当 GC 处于 GCSpropagate 状态(mark 流程中),就标记 v 。否则,把与之联系的节点 o 从黑色置为白色。前面我们已经说过,lua 的 GC 采用了两种白色标记,乒乓切换。在 mark 流程结束后,白色状态被切换。这个时候调用 makewhite 并不会导致 sweep 过程清除这个节点。同时,由于 o 变为白色,就可以减少 barrier 的开销。即,再有对 o 的修改,不会产生 luaC_barrierf 的函数调用了。

luaC_barrierback 会把要修改的 table 退回 gary 集合,留待将来继续标记。见代码 lgc.c 的 676 行:

void luaC_barrierback (lua_State *L, Table *t) {global_State *g = G(L);
GCObject *o = obj2gco(t);
lua_assert(isblack(o) && !isdead(g, o));
lua_assert(g->gcstate != GCSfinalize && g->gcstate != GCSpause);
black2gray(o);  /* make table gray (again) */
t->gclist = g->grayagain;
g->grayagain = o;
}

这里提到了 grayagain 链,昨天已经看到,这个链表会推迟到 atomic 阶段处理。把 table 设会灰色可使后续对其的修改不再触发 luaC_barrierback

注意,对弱表的修改是不会触发 luaC_barrierback 的。因为弱表不会是黑色。弱表也不能触发它。因为每个 GCObject 都只能存在于一个链表上。弱表自己挂接到 weak 链,就不能向 grayagain 挂接。这就解释了 lgc.c 285 行:

if (traversetable(g, h))  /* table is weak? */
black2gray(o);  /* keep it gray */

为何要把弱表从黑色设回灰色。

因为弱表又分 key weak 和 value weak ,它倒不一定完全都包含了弱引用。在扫描过程中,对弱表的修改又不会触发 barrier 。这会导致最终扫描完成后,某些弱表里可能还藏有一些新建立的强应用关系。到这里,我们便可以解释昨天遗留的另一个问题,弱表为何到了 atomic 节点需要再次 mark 。见 lgc.c 的 533 行:

/* remark weak tables */
g->gray = g->weak;
g->weak = NULL;
lua_assert(!iswhite(obj2gco(g->mainthread)));
markobject(g, L);  /* mark running thread */
markmt(g);  /* mark basic metatables (again) */
propagateall(g);

再次 mark basic metatables 的原因是,设置基本类型的全局 metatable 并没有设置 write barrier (这组 metatable 比较特殊,不适合用以上的 barrier 函数)。见 lapi.c 的 710 行:

switch (ttype(obj)) {case LUA_TTABLE: {hvalue(obj)->metatable = mt;
if (mt)
luaC_objbarriert(L, hvalue(obj), mt);
break;
}
case LUA_TUSERDATA: {uvalue(obj)->metatable = mt;
if (mt)
luaC_objbarrier(L, rawuvalue(obj), mt);
break;
}
    default: {G(L)->mt[ttype(obj)] = mt;
break;
}
}

这里,修改 table 的 metatable 调用了 luaC_objbarriert ,修改 userdata 的 metatable 调用了 luaC_objbarrier ,修改全局 metatable 是直接进行的。

如果审查所有的代码,会发现 barrier 的调用分布在很多地方。大多数情况下,barrier 的开销并不大,就是一次条件判断。但 barrier 也会被执行很频繁,性能必须有保障,这也是为什么 barrier 相关 api 都用宏来实现的缘故。


理解了 write barrier 的实现,我们就可以回头来看 propagatemark 过程了。

propagatemark 每次会标记一个灰色对象所有的关联节点,并把自己置为黑色。见 lgc.c 的 273 行。

/*
** traverse one gray object, turning it to black.
** Returns `quantity' traversed.
*/
static l_mem propagatemark (global_State *g) {GCObject *o = g->gray;
lua_assert(isgray(o));
gray2black(o);
switch (o->gch.tt) {

之后就是对需要迭代标记的具体类型的处理了。

对 table 的处理是这样的,见 lgc.c 的 282 行:

case LUA_TTABLE: {Table *h = gco2h(o);
g->gray = h->gclist;
if (traversetable(g, h))  /* table is weak? */
black2gray(o);  /* keep it gray */
return sizeof(Table) + sizeof(TValue) * h->sizearray +
sizeof(Node) * sizenode(h);
}

从 gray 链上取掉当前 table 节点,然后迭代这个 table ,如果是一个弱表,则把其还原为灰色。

traversetable 枚举了 table 中的所有引用项。并对弱表做了处理。见 lgc.c 的 166 行:

if (mode && ttisstring(mode)) {  /* is there a weak mode? */
weakkey = (strchr(svalue(mode), 'k') != NULL);
weakvalue = (strchr(svalue(mode), 'v') != NULL);
if (weakkey || weakvalue) {  /* is really weak? */
h->marked &= ~(KEYWEAK | VALUEWEAK);  /* clear bits */
h->marked |= cast_byte((weakkey << KEYWEAKBIT) |
(weakvalue << VALUEWEAKBIT));
h->gclist = g->weak;  /* must be cleared after GC, ... */
g->weak = obj2gco(h);  /* ... so put in the appropriate list */
}
}

被扫描到的弱表把自己挂到了 weak 链上。单独保存一个链表是因为弱表在扫描的最后阶段还需要把里面不存在的弱引用项清理掉。关于清理工作,是由 atomic 函数中对 cleartable 调用完成的。昨天已经讲过,无庸赘述。

对 function 的 mark 工作在 traverseclosure 函数中进行,lgc.c 224 行:

static void traverseclosure (global_State *g, Closure *cl) {markobject(g, cl->c.env);
if (cl->c.isC) {int i;
for (i=0; i<cl->c.nupvalues; i++)  /* mark its upvalues */
markvalue(g, &cl->c.upvalue[i]);
}
else {int i;
lua_assert(cl->l.nupvalues == cl->l.p->nups);
markobject(g, cl->l.p);
for (i=0; i<cl->l.nupvalues; i++)  /* mark its upvalues */
markobject(g, cl->l.upvals[i]);
}
}

lua 中 C Function 和 Lua Function 的内部结构是不同的。所以是分别独立的代码。主要就是对 upvalue 以及环境表的标记了。

对 thread 的处理相对复杂一点。见 lgc.c 297 行:

case LUA_TTHREAD: {lua_State *th = gco2th(o);
g->gray = th->gclist;
th->gclist = g->grayagain;
g->grayagain = o;
black2gray(o);
traversestack(g, th);
return sizeof(lua_State) + sizeof(TValue) * th->stacksize +
sizeof(CallInfo) * th->size_ci;
}

这里的链表操作可以简述为,把自己从 gray 链上摘下,放到 grayagain 链表中。并把自己保持为灰色状态。

这是因为 thread 关联的对象其实是运行堆栈,堆栈是随着运行过程不断变化的。如果目前尚处于 mark 流程没有结束的话,后面发生变化是很有可能的。而 stack 上数据的修改是不经过 write barrier 的。也没有必要为最频繁的 stack 修改做 barrier ,那样对性能影响就很大了。最简单的办法就是把 thread 推迟到 atomic 阶段重扫描一次。

atomic 函数,lgc.c 537 行的

markobject(g, L);  /* mark running thread */

也是这个用意。

ps. lua 的 GC 在实现时,即使要推迟扫描,也要先做一次。都是为了 atomic 阶段尽量短,把有可能遍历的部分先遍历完。

TPROTO 对象的遍历函数在 lgc.c 的 199 行:

/*
** All marks are conditional because a GC may happen while the
** prototype is still being created
*/
static void traverseproto (global_State *g, Proto *f) {int i;
if (f->source) stringmark(f->source);
for (i=0; i<f->sizek; i++)  /* mark literals */
markvalue(g, &f->k[i]);
for (i=0; i<f->sizeupvalues; i++) {  /* mark upvalue names */
if (f->upvalues[i])
stringmark(f->upvalues[i]);
}
for (i=0; i<f->sizep; i++) {  /* mark nested protos */
if (f->p[i])
markobject(g, f->p[i]);
}
for (i=0; i<f->sizelocvars; i++) {  /* mark local-variable names */
if (f->locvars[i].varname)
stringmark(f->locvars[i].varname);
}
}

如注释所言,这里有很多的判断。这缘于 prototype 的创建过程是可能分步的,中间有可能被 GC 打断。prototype 的创建伴随着诸多 GCObject 的创建。新对象的创建(大多数是 string)带来的是新的内存申请,很容易触发自动 GC 的进行。如果对 prototype 的创建过程有兴趣,可以去阅读 lparser.c 的 luaY_parser 函数,以及 lundump.c 中的 luaU_undump 函数。

以上就是所有需要迭代遍历的数据类型了。

一切准备妥当后,GC 将切换到 GCSsweepstring 。见 lgc.c 548 行:

/* flip current white */
g->currentwhite = cast_byte(otherwhite(g));
g->sweepstrgc = 0;
g->sweepgc = &g->rootgc;
g->gcstate = GCSsweepstring;
g->estimate = g->totalbytes - udsize;  /* first estimate */

这时,userdata 的 gc 元方法虽未调用,所有不再有引用的 userdata 不会在这趟 GC 环节中清理出去,但它们占据的空间已经被 estimate 忽略。

lua_gc 源码学习五相关推荐

  1. SDN控制器Floodlight源码学习(五)--控制器和交换机交互(3)

    上两篇诣在说明控制器和交换机的交互,但是感觉还是没有理的特别清楚 http://blog.csdn.net/crystonesc/article/details/70143117 http://blo ...

  2. lua_gc 源码学习二

    普及下常识:GC 是 garbage collector 资源回收器: 初期的 Lua GC 采取的是 stop the world 的实现.一旦产生 gc 就需要期待全部 gc 流程走完.lua 自 ...

  3. mutations vuex 调用_Vuex源码学习(六)action和mutation如何被调用的(前置准备篇)...

    前言 Vuex源码系列不知不觉已经到了第六篇.前置的五篇分别如下: 长篇连载:Vuex源码学习(一)功能梳理 长篇连载:Vuex源码学习(二)脉络梳理 作为一个Web前端,你知道Vuex的instal ...

  4. action mutation 调用_Vuex源码学习(六)action和mutation如何被调用的(前置准备篇)...

    module与moduleCollection你一定要会啊!Vuex源码学习(五)加工后的module 在组件中使用vuex的dispatch和commit的时候,我们只要把action.mutati ...

  5. Vuex 4源码学习笔记 - 通过dispatch一步步来掌握Vuex整个数据流(五)

    在上一篇笔记中:Vuex 4源码学习笔记 - Store 构造函数都干了什么(四) 我们通过查看Store 构造函数的源代码可以看到主要做了三件事情: 初始化一些内部变量以外 执行installMod ...

  6. Vuex源码学习(五)加工后的module

    没有看过moduleCollection那可不行!Vuex源码学习(四)module与moduleCollection 感谢提出代码块和截图建议的小伙伴 代码块和截图的区别: 代码块部分希望大家按照我 ...

  7. JDK源码学习笔记——Integer

    一.类定义 public final class Integer extends Number implements Comparable<Integer> 二.属性 private fi ...

  8. Java多线程之JUC包:Semaphore源码学习笔记

    若有不正之处请多多谅解,并欢迎批评指正. 请尊重作者劳动成果,转载请标明原文链接: http://www.cnblogs.com/go2sea/p/5625536.html Semaphore是JUC ...

  9. JDK源码学习笔记——Enum枚举使用及原理

    一.为什么使用枚举 什么时候应该使用枚举呢?每当需要一组固定的常量的时候,如一周的天数.一年四季等.或者是在我们编译前就知道其包含的所有值的集合. 利用 public final static 完全可 ...

最新文章

  1. 服务器系统需要定期清理吗,windows 2008服务器系统清理
  2. alpine运行程序提示not found
  3. 特斯拉副总裁陶琳:Model Y 本月开始陆续交付
  4. 【Java学习笔记之三十一】详解Java8 lambda表达式
  5. Comega -- O/R Mapper的终结者?
  6. Pandownload作者被抓后,我决定用docker搭建一款私有网盘
  7. office创建数据透视表
  8. 【PS】如何把图片变成透明
  9. 有水印的图片简直不要太尬!教你去水印,以后精美图片统统拿走!
  10. 微信小程序| 做一款可以计算亲戚关系的计算器
  11. 一步教你超简单设置Android Studio背景图片
  12. 优化算法系列-模拟退火算法(1)——基本原理枯燥版本
  13. 【PyTorch】提高mnist模型精确度
  14. 参考行标对云效以及LinKE的“持续交付”及“敏捷开发管理”能力打了下分,大家看肿么样?
  15. 为什么脑子里有电波声_为什么有时会听到“电波声”?
  16. 2016年8月15日 星期一 --出埃及记 Exodus 16:16
  17. 为什么SHL和SAL相同
  18. 连接mysql报错 errorCode 1129, state HY000, Host ‘xxx‘ is blocked because of many connection errors
  19. n(n-1)/2的奇偶性
  20. 15年前的元宇宙报告-Metaverse Roadmap (2007年)

热门文章

  1. 如何判断段码液晶屏是没有问题的?
  2. MiniTool帮我找回重要合同
  3. 云游昆大丽(四)——阳光丽江
  4. 多屏互动——H5中级进阶
  5. HTML5期末大作业:榆林子州网站设计——榆林子州-含论文(6页) 榆林子州旅游网页HTML代码 学生网页设计与制作期末作业下载 大学生网页设计与制作成品下载 DW旅行网页作业
  6. rust盖错了怎么拆除_Rust可恢复的错误
  7. 【转载】matlab添加路径
  8. [转]ASP.NET MVC3 新手教程:Hellow简单示例
  9. 万博思图-HPLC数据可视化分析引擎
  10. Python 中的多层for in嵌套循环使用