最近接手的一个游戏项目是重 Lua
的结构(网络模块在 C++,其余逻辑全在 Lua)。和许多用 Lua 的游戏项目一样,遇到了 Lua 的垃圾回收的性能问题,经常跑着跑着就会掉帧,因此花了一周的时间,给 Lua 虚拟机写了个模块,把 Lua 垃圾回收的速度提高了一个量级。
思路
这个思路其实在之前的一篇博客中也有提到,想要 垃圾回收快,无非就那么几种思路。
- 使用内存池
- 减少对象生成
- 垃圾回收提速
使用内存池
第一种思路,我觉得不合理,因为现代的内存分配器早就有内存池的设计了,手写一个内存池的收益并不大。
减少对象生成
第二种思路,是比较合理的。因为我在项目的代码中发现很多处地方有动态生成 Closure
的情况。
1 | function test() |
上面那个例子,每次调用到 test
函数的时候,都会动态根据 fn
的 函数原型,生成一个 Closure
可能有人会问,Proto 不是有一个 cache 指向 Closure
吗?按道理这里 没有 UpValue
(即代表UpValue 完全相同),应该会复用啊,但是很可惜,执行完这个函数以后,因为没有对象指向 Closure
用完再不久的将来又会被回收。
因此,少写这种代码就可以减少对象的生成。
垃圾回收提速
第三种思路,我的想法是,让垃圾回收所要遍历的对象大幅减少,就可以为垃圾回收提速了,由于我们是重 Lua 的框架,因此我们的所有配置都存在于 Lua 的 table中,而这一部分肯定是不需要被回收的,但是每次垃圾回收的时候,又会不停的扫描递归遍历,不合理。同时代码中的很多全局函数,也是根本不需要被回收的,也会被扫描到,于是就想到一个想法,给这些对象打上标记,让他们不被遍历不被清理,就可以大幅度的提速了。
原理简单,但是做起来确实挺难受的,要注意要手动关闭 UpValue
将其保留下来。
目前已经开源,LuaJIT-5.3.6源码。
如何使用呢?
说的那么好,那如何使用呢?
目前提供了四个接口。
1 | nogc("open", Table) -- 这一整个 Table 都不被扫描不被清理 |
Table 中的元素支持,字符串,整数,浮点数,布尔值,表,Lua 闭包。
不支持 当 Table 是弱表的情况。
需要注意的是,当一个 Table 被打上标记之后,就不能够再修改其内部的数据,因为有可能会创建出一个新的对象,但是又不会被 Lua 的垃圾回收扫描到,导致这个对象被回收,发生段错误。
接入 Lua
首先需要引入我写的两个文件, YGC.c, YGC.h
。
然后跟着我的步伐修改以下几个文件。
lbaselib.c
添加头文件,然后导出 nogc
函数给 Lua 使用。
1 |
|
lvm.c
添加头文件,在 pushclosure
函数这里, if (!isblack(p))
改为以下的代码。
这是因为,当我们标记的 Table 中含有的闭包,被执行到的时候,会动态的生成 Closure
,但是这个 Closure
是没办法被标记到的,因为是动态生成的,因此不应该指过去。
1 |
|
lstate.h
在 global_State
记录两个辅助的值,其中一个是 nogc 的对象内存大小,另一个是 不参与GC的链表,都是为了方便调试用的。
1 | typedef struct global_State { |
lstate.c
初始化上面的对象
1 | LUA_API lua_State *lua_newstate (lua_Alloc f, void *ud) { |
lgc.c
这是最后一个文件了,依然是添加头文件。然后将以下代码进行对比替换。
1 |
提前返回对象,减少垃圾回收耗时。
将 以下代码 进行替换,简单的来说就是将不需要GC的对象,移出 allgc
链表。
1 | static GCObject **sweeplist (lua_State *L, GCObject **p, lu_mem count) { |
替换为以下这段。
1 | static GCObject **sweeplist (lua_State *L, GCObject **p, lu_mem count) { |
propagatemark 的修改主要是为了提前返回,不要遍历不需要GC的对象。
1 | static void propagatemark (global_State *g) { |
替换为。
1 | static void propagatemark (global_State *g) { |
至此完结,享受提速后的快感吧。