最近接手的一个游戏项目是重 Lua
的结构(网络模块在 C++,其余逻辑全在 Lua)。和许多用 Lua 的游戏项目一样,遇到了 Lua 的垃圾回收的性能问题,经常跑着跑着就会掉帧,因此花了一周的时间,给 Lua 虚拟机写了个模块,把 Lua 垃圾回收的速度提高了一个量级。
思路 这个思路其实在之前的一篇博客中也有提到,想要 垃圾回收快,无非就那么几种思路。
使用内存池 减少对象生成 垃圾回收提速 使用内存池 第一种思路,我觉得不合理,因为现代的内存分配器早就有内存池的设计了,手写一个内存池的收益并不大。
减少对象生成 第二种思路,是比较合理的。因为我在项目的代码中发现很多处地方有动态生成 Closure
的情况。
1 2 3 4 5 6 7 function test () local fn = function () print ("test" ) end fn() fn() end
上面那个例子,每次调用到 test
函数的时候,都会动态根据 fn
的 函数原型,生成一个 Closure
可能有人会问,Proto 不是有一个 cache 指向 Closure
吗?按道理这里 没有 UpValue
(即代表UpValue 完全相同),应该会复用啊,但是很可惜,执行完这个函数以后,因为没有对象指向 Closure
用完再不久的将来又会被回收。
因此,少写这种代码就可以减少对象的生成。
垃圾回收提速 第三种思路,我的想法是,让垃圾回收所要遍历的对象大幅减少,就可以为垃圾回收提速了,由于我们是重 Lua 的框架,因此我们的所有配置都存在于 Lua 的 table中,而这一部分肯定是不需要被回收的,但是每次垃圾回收的时候,又会不停的扫描递归遍历,不合理。同时代码中的很多全局函数,也是根本不需要被回收的,也会被扫描到,于是就想到一个想法,给这些对象打上标记,让他们不被遍历不被清理,就可以大幅度的提速了。
原理简单,但是做起来确实挺难受的,要注意要手动关闭 UpValue
将其保留下来。
目前已经开源,LuaJIT-5.3.6 源码。
如何使用呢? 说的那么好,那如何使用呢?
目前提供了四个接口。
1 2 3 4 nogc("open" , Table) nogc("close" , Table) nogc("len" ) nogc("count" )
Table 中的元素支持,字符串,整数,浮点数,布尔值,表,Lua 闭包。
不支持 当 Table 是弱表的情况。
需要注意的是,当一个 Table 被打上标记之后,就不能够再修改其内部的数据,因为有可能会创建出一个新的对象,但是又不会被 Lua 的垃圾回收扫描到,导致这个对象被回收,发生段错误。
接入 Lua 首先需要引入我写的两个文件, YGC.c, YGC.h
。
然后跟着我的步伐修改以下几个文件。
lbaselib.c 添加头文件,然后导出 nogc
函数给 Lua 使用。
1 2 3 4 5 6 7 8 #include "YGC.h" static const luaL_Reg base_funcs[] = { .... {"nogc" , nogc}, {NULL , NULL } };
lvm.c 添加头文件,在 pushclosure
函数这里, if (!isblack(p))
改为以下的代码。
这是因为,当我们标记的 Table 中含有的闭包,被执行到的时候,会动态的生成 Closure
,但是这个 Closure
是没办法被标记到的,因为是动态生成的,因此不应该指过去。
1 2 3 4 5 6 #include "YGC.h" if (!isblack(p) && !Y_isnogc(p) && !Y_isnogc(ncl)) p->cache = ncl; }
lstate.h 在 global_State
记录两个辅助的值,其中一个是 nogc 的对象内存大小,另一个是 不参与GC的链表,都是为了方便调试用的。
1 2 3 4 5 6 typedef struct global_State { .... lu_mem Y_GCmemnogc; GCObject *Y_nogc; .... }
lstate.c 初始化上面的对象
1 2 3 4 5 6 LUA_API lua_State *lua_newstate (lua_Alloc f, void *ud) { .... g->Y_GCmemnogc = 0 ; g->Y_nogc = NULL ; .... }
lgc.c 这是最后一个文件了,依然是添加头文件。然后将以下代码进行对比替换。
提前返回对象,减少垃圾回收耗时。
将 以下代码 进行替换,简单的来说就是将不需要GC的对象,移出 allgc
链表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 static GCObject **sweeplist (lua_State *L, GCObject **p, lu_mem count) { global_State *g = G(L); int ow = otherwhite(g); int white = luaC_white(g); while (*p != NULL && count-- > 0 ) { GCObject *curr = *p; int marked = curr->marked; if (isdeadm(ow, marked)) { *p = curr->next; freeobj(L, curr); } else { curr->marked = cast_byte((marked & maskcolors) | white); p = &curr->next; } } return (*p == NULL ) ? NULL : p; }
替换为以下这段。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 static GCObject **sweeplist (lua_State *L, GCObject **p, lu_mem count) { global_State *g = G(L); int ow = otherwhite(g); int white = luaC_white(g); while (*p != NULL && count-- > 0 ) { GCObject *curr = *p; if (g->gcstate == GCSswpallgc && Y_isnogc(curr)) { *p = curr->next; curr->next = g->Y_nogc; g->Y_nogc = curr; continue ; } int marked = curr->marked; if (isdeadm(ow, marked)) { *p = curr->next; freeobj(L, curr); } else { curr->marked = cast_byte((marked & maskcolors) | white); p = &curr->next; } } return (*p == NULL ) ? NULL : p; }
propagatemark 的修改主要是为了提前返回,不要遍历不需要GC的对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 static void propagatemark (global_State *g) { lu_mem size; GCObject *o = g->gray; lua_assert(isgray(o)); gray2black(o); switch (o->tt) { case LUA_TTABLE: { Table *h = gco2t(o); g->gray = h->gclist; size = traversetable(g, h); break ; } case LUA_TLCL: { LClosure *cl = gco2lcl(o); g->gray = cl->gclist; size = traverseLclosure(g, cl); break ; } case LUA_TCCL: { CClosure *cl = gco2ccl(o); g->gray = cl->gclist; size = traverseCclosure(g, cl); break ; } case LUA_TTHREAD: { lua_State *th = gco2th(o); g->gray = th->gclist; linkgclist(th, g->grayagain); black2gray(o); size = traversethread(g, th); break ; } case LUA_TPROTO: { Proto *p = gco2p(o); g->gray = p->gclist; size = traverseproto(g, p); break ; } default : lua_assert(0 ); return ; } g->GCmemtrav += size; }
替换为。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 static void propagatemark (global_State *g) { lu_mem size; GCObject *o = g->gray; lua_assert(isgray(o)); gray2black(o); switch (o->tt) { case LUA_TTABLE: { Table *h = gco2t(o); g->gray = h->gclist; size = (Y_isnogc(o) ? 0 : traversetable(g, h)); break ; } case LUA_TLCL: { LClosure *cl = gco2lcl(o); g->gray = cl->gclist; size = (Y_isnogc(cl) ? 0 : traverseLclosure(g, cl)); break ; } case LUA_TCCL: { CClosure *cl = gco2ccl(o); g->gray = cl->gclist; size = traverseCclosure(g, cl); break ; } case LUA_TTHREAD: { lua_State *th = gco2th(o); g->gray = th->gclist; linkgclist(th, g->grayagain); black2gray(o); size = traversethread(g, th); break ; } case LUA_TPROTO: { Proto *p = gco2p(o); g->gray = p->gclist; size = (Y_isnogc(p) ? 0 : traverseproto(g, p)); break ; } default : lua_assert(0 ); return ; } g->GCmemtrav += size; }
至此完结,享受提速后的快感吧。