首页 > 代码库 > lua_gc 源码学习三

lua_gc 源码学习三

我们晓得,lua 对外的 API 中,统统个 gc 打交道的都经过lua_gc。C 说话构建体系时,普通不讲计划模式。但模式仍是存在的。若要按《计划模式》中的分类,这应当归于 Facade 形式。代码在 lapi.c 的 895 行:
 LUA_API int lua_gc (lua_State *L, int what, int data) { int res = 0; global_State *g; lua_lock(L); g = G(L); switch (what) { case LUA_GCSTOP: g->GCthreshold = MAX_LUMEM; break; case LUA_GCRESTART: g->GCthreshold = g->totalbytes; break; case LUA_GCCOLLECT: luaC_fullgc(L); break; case LUA_GCCOUNT: res = cast_int(g->totalbytes >> 10); break; case LUA_GCCOUNTB: res = cast_int(g->totalbytes & 0x3ff); break; case LUA_GCSTEP: { lu_mem a = (cast(lu_mem, data) << 10); if (a <= g->totalbytes) g->GCthreshold = g->totalbytes - a; else g->GCthreshold = 0; while (g->GCthreshold <= g->totalbytes) { luaC_step(L); if (g->gcstate == GCSpause) res = 1; break; } break; } case LUA_GCSETPAUSE: res = g->gcpause; g->gcpause = data; break; case LUA_GCSETSTEPMUL: res = g->gcstepmul; g->gcstepmul = data; break; default: res = -1; } lua_unlock(L); return res; }

从代码可见,对核心状态的会见,都是干脆接见 global state 表的。GC 控制则是调用内部 api 。lua 中对外的 api 和内部模块交互的 api 都是分隔的。这样井井有条。内部子模块平常名为luaX_xxxX 为子模块代号。对收集器相干的 api 一概以luaC_xxx定名。这些 api 定义在 lgc.h 中。

其间提到的 api 有两个:

LUAI_FUNC void luaC_step (lua_State *L); LUAI_FUNC void luaC_fullgc (lua_State *L);

用于分步 GC 已经完备 GC 。

另外一个主要的 api 是:

#define luaC_checkGC(L) \ condhardstacktests(luaD_reallocstack(L, L->stacksize - EXTRA_STACK - 1)); \ if (G(L)->totalbytes >= G(L)->GCthreshold) \ luaC_step(L);

它以宏情势定义进去,用于主动的 GC 。如果我们检查 lapi.c ldo.c lvm.c ,会发明大部门会导致内存增加的 api 中,都挪用了它。包管 gc 可以随内存利用增长而主动停止。

这里插几句。

使用自动 gc 会有一个题目。它极可能使体系的峰值内存占用远超过实际需要量。缘由就在于,收集行动每每产生在调用栈很深之处。当你的利用法式显现出某种周期性(大大都包驱动的办事都是这样)。在一个办事周期内,常常会援用浩繁姑且对象,这个时辰做 mark 工作,会导致很多且则对象也被 mark 住。

一个阅历方式是,调用LUA_GCSTOP遏制自动 GC。在周时代按期调用 gcstep 且使用较大的 data 值,在无限个周期做完一整趟 gc 。

另,condhardstacktests 是一个宏,一般为不打开的。

先来看luaC_fullgc。它用来执行完全的一次 gc 行动。fullgc 并非只是把当前的流程走完。由于以前的 gc 行动可能执行了一半,大概有一些半途加出去的需要收受接管的对象。所以在走完一趟流程后,fullgc 将梗阻着再完好跑一遍 gc 。整个流程有一些优化的余地。即,前半程的 gc 流程并不必严厉执行,它其实不需要真的去断根什么。只要要把状态规复。这个工作是怎样做到的呢?见 lgc.c 的 637 行:

void luaC_fullgc (lua_State *L) { global_State *g = G(L); if (g->gcstate <= GCSpropagate) g->sweepstrgc = 0; g->sweepgc = &g->rootgc; g->gray = NULL; g->grayagain = NULL; g->weak = NULL; g->gcstate = GCSsweepstring; lua_assert(g->gcstate != GCSpause && g->gcstate != GCSpropagate); while (g->gcstate != GCSfinalize) lua_assert(g->gcstate == GCSsweepstring

比力耗时的 mark 步调被简略跳过了(如果它还没举行完的话)。和一般的 mark 流程分歧,平常的 mark 流程末了,会将红色标识表记标帜反转。见 lgc.c 548 行,atomic 函数:

 g->currentwhite = cast_byte(otherwhite(g));

在 fullgc 的前半程中,间接跳过了 GCSpropagate ,重置了外部状况,但没有翻转白色标志。这会导致背面的 sweep 流程不会真的开释那些白色工具。sweep 工作现实做的仅仅把一切工具又从新设置回白色罢了。

接下来便是一个完好不被打断的 gc 历程了。

markroot(L); while (g->gcstate != GCSpause) singlestep(L); setthreshold(g);

从根起头 mark ,直到全部 gc 流程履行终了。最终,从头配置了 GCthreshold 。注:调用 fullgc 会重置 GCthreshold ,以是如果你曾挪用LUA_GCSTOP停息自动 GC 的话(也是经由过程点窜 GCthreshold 完成),记得再调用一次。

stepgc 要相对于庞大一些。在 lua 手册的 2.10 诠释了 garbage-collector pause 和 step multiplier 的意思,却不给出切确界说。lua_gc的申明里,也只说“LUA_GCSTEP: 倡议一步增量渣滓收集。步数由 data 控制(越大的值象征着越多步), 而其详细寄义(具体数字暗示了几多)并未尺度化。假如你想控制这个步数,必需尝试性的测试 data 的值。 要是这一步完成了一个废物收集周期,返回返回 1 。并无给出精确的寄义。理论中,我们也都因此经历取值。

回到源代码,我们就可以搞清晰它们究竟是甚么了。

case LUA_GCSETPAUSE: res = g->gcpause; g->gcpause = data; break; case LUA_GCSETSTEPMUL: res = g->gcstepmul; g->gcstepmul = data; break;

这里只是设置 gcpause gcstepmul 。gcpause 实际只在 lgc.c 59 行的 setthreshold 宏顶用到

#define setthreshold(g) (g->GCthreshold = (g->estimate/100) * g->gcpause)

瞥见,GCSETPAUSE 实际上是通过调剂 GCthreshold 来实现的。当 GCthreshold 充足大时,luaC_step不会被luaC_checkGC自动触发。究竟上,GCSTOP 恰是通过设置一个很大的 GCthreshold 值来实现的。

case LUA_GCSTOP: g->GCthreshold = MAX_LUMEM; break;

gcpause 值的含义很文档分歧,用来示意和现实内存利用量 estimate 的比值(扩大 100 倍)。一旦内存使用量超出这个阀值,就会动身 GC 的工作。

要明白 gcstepmul ,就要从lua_gc的LUA_GCSTEP的实现看起。

case LUA_GCSTEP: { lu_mem a = (cast(lu_mem, data) << 10); if (a <= g->totalbytes) g->GCthreshold = g->totalbytes - a; else g->GCthreshold = 0; while (g->GCthreshold <= g->totalbytes) { luaC_step(L); if (g->gcstate == GCSpause) res = 1; break; } break; }

step 的长度 data 被扩大了 1024 倍。在 lgc.c 的 26 行,也能够看到

#define GCSTEPSIZE 1024u

咱们权且能够以为 data 的单元是 KBytes ,和 lua 统共占用的内存 totalbytes 有些干系。

ps. 这里 totalbytes 是严格通过 Alloc 办理的内存量,比来。而后面提到的 estimate 则差别,它是一个预算量,比 totalbytes 要小。这是因为,前方也提到过,userdata 的接纳对照特别。被检测出已经拜候不到的 userdata 占用的内存并不会顿时开释(担保 gc 元要领的平安调用),但 estimate 会抛去这部份,不算在实际内存使用量内。

见 lgc.c 544 行

udsize = luaC_separateudata(L, 0);

和 lgc.c 553 行

g->estimate = g->totalbytes - udsize;

从代码逻辑,咱们临时可以把 data 了解为,必要处置惩罚的字节数目(以 K bytes 为单元)。若是需求处置的数据量跨越了 totalbytes ,天然就能够把 GCthreshold 配置为 0 了。

实际上不克不及完整这么明白。因为 GC 过程并不是一点点回收内存,同时可用内存愈来愈多。GC 分符号(mark) 肃清(sweep) 调用 userdata 元方法等几个阶段。只要中心的排除阶段是真实释放内存的。所以可用内存的增加( totalbytes 削减)过程,时候上并不是线性的。每每标志的开消更大。为了让 gcstep 的每一个程序耗损的时间更光滑,就得有手腕静态调解 GCthreshold 值。它和 totalbytes 终究影响了每一个 step 的时间。

上面的存眷核心转向luaC_step ,见 lgc.c 的 611 行:

void luaC_step (lua_State *L) { global_State *g = G(L); l_mem lim = (GCSTEPSIZE/100) * g->gcstepmul; if (lim == 0) lim = (MAX_LUMEM-1)/2; g->gcdept += g->totalbytes - g->GCthreshold; do lim -= singlestep(L); if (g->gcstate == GCSpause) break; while (lim > 0); if (g->gcstate != GCSpause) { if (g->gcdept < GCSTEPSIZE) g->GCthreshold = g->totalbytes + GCSTEPSIZE; else g->gcdept -= GCSTEPSIZE; g->GCthreshold = g->totalbytes; } else lua_assert(g->totalbytes >= g->estimate); setthreshold(g); }

从代码我们可以看到,GC 的焦点其其实于 singlestep 函数。luaC_step屡次调用几何次 singlestep 跟 gcstepmul 的值相关。

如果是自动进行的 GC ,当 totalbytes 大于即是 GCthreshold 时,就会触发luaC_step。每次luaC_step,GCthreshold 城市被调高 1K (GCSTEPSIZE) 直到 GCthreshold 追上 totalbytes 。这个追逐进程凡是产生在 mark 流程。由于这个流程中,totalbytes 是只增不减的。

如果是手控 GC ,屡次 gcstep 调用履行几许次luaC_step则跟 data 值相关。大致上是 1 就默示一次(在 mark 过程当中便是如许)到了 sweep 流程就未必了。这和 singlestep 调用次数,即 gcstepmul 的值有关。它影响了 totalbytes 的减小速度。

以是,一两句话很难严厉定义出这些控制 GC 步进量的参数的含义,只能渐渐浏览代码,看看实现了。

在 lua 手册的 2.10 如许描写“step multiplier 节制了收集器相对于内存分派的速率。更大的数字将致使收集器事情的更自动的同时,也使每步搜集的尺寸增添。 小于 1 的值会使收集器劳动的很是慢,大概导致搜集器永久都竣事不了以后周期。 缺省值为 2 ,这象征着收集器将之内存分派器的两倍速运转。”

从代码看,这绝非严酷界说。最少从本日已阐发的代码中还看不出这一点。

lua_gc 源码学习三