1. 程式人生 > >lua_gc 原始碼學習二

lua_gc 原始碼學習二

普及下常識:GC 是 garbage collector 資源回收器;

初期的 Lua GC 採取的是 stop the world 的實現。一旦產生 gc 就需要期待全部 gc 流程走完。lua 自己是個很精簡的體系,但不代表處理的資料量也必然很小。

從 Lua 5.1 入手下手,GC 的實現改成分步的。固然照舊是 stop the world ,可是,每個步驟均可以分階段執行。這樣,屢次擱淺的時間較小。隨之,這部門的程式碼也相對於紛亂了。分步執行最關鍵的問題是需要處理在 GC 的步驟之間,如果資料關聯的狀態發生了變化,如何保證 GC 的準確性。GC 的分步執行相對一次執行完,總的時候開銷的不同並非零代價的。只是在實現上,要儘可能讓額外增長的價錢較小。

先來看 GC 流程的分別。

lua 的 GC 分為五個大的階段GC 處於哪一個階段(程式碼中被稱為狀態),根據的是global_State中的 gcstate 域。狀態以巨集式樣界說在 lgc.h 的 14 行。

/*
** Possible states of the Garbage Collector
*/
#define GCSpause    0
#define GCSpropagate    1
#define GCSsweepstring  2
#define GCSsweep    3
#define GCSfinalize 4

狀態的值的鉅細也表示著它們的履行順序。需要注重的是,GC 的執行歷程並不是每程式都堵塞在一個狀況上。

GCSpause 階段是每一個 GC 流程的啟始步調。僅僅標識表記標幟體系的根節點。見 lgc.c 的 561 行。

switch (g->gcstate) 
{
    case GCSpause: markroot(L);  /* start a new collection */
      return 0;

markroot 這個函式所做之事,就是標記主執行緒物件,標記主執行緒的區域性表、登錄檔,以及為區域性範例註冊的元表。標記的詳細過程我們背面再講。

GCSpause 階段執行完,立即就將狀態切換到了 GCSpropagate 。這是一個標記流程。這個流程會分步結束。當檢測到另有物件待標記時,迭代標記(重複呼叫 propagatemark);終究,會有一個標記功程不可被打斷,這些操縱放在一個叫作 atomic 的函式中執行

。見 lgc.c 的 565 行:

case GCSpropagate: 
{
      if (g->gray)
        return propagatemark(g);
      else /* no more `gray' objects */
        atomic(L);  /* finish mark phase */
        return 0;
}

這裡大概需要順帶提一下的是 gray 域。望文生義,它指 GCObject 中的灰色節點鏈。作為灰色,即處於白色和黑色之間的狀態。對於節點的顏色,頓時就會闡明。

接下來就是清除流程了。

前面我們提到過,string 在 Lua 中是單獨經管的,以是也需要單獨清除。GCSsweepstring 階段乾的就是這個事。string table 以 hash 表形式治理所有的 string 。GCSsweepstring 中,每個步驟(step) 清理 hash 表的一列。程式碼見 lgc.c 的 573 行

case GCSsweepstring: lu_mem old = g->totalbytes;
      sweepwholelist(L, &g->strt.hash[g->sweepstrgc++]);
      if (g->sweepstrgc >= g->strt.size)  /* nothing more to sweep? */
        g->gcstate = GCSsweep;  /* end sweep-string phase */
      lua_assert(old >= g->totalbytes);
      g->estimate -= old - g->totalbytes;
      return GCSWEEPCOST;

這裡可以看到 estimate 和 totalbytes 兩個域,從名字上可以知道,它們別離暗示了 lua vm 佔用的記憶體位元組數以及實際分派的位元組數。

ps. 如果你本身實現過記憶體管理器,當知道記憶體治理自己是有分外的記憶體開銷的。如果有需要確切控制記憶體數目,我偏向於連續記憶體管理器統計精確的記憶體使用環境。比如你向記憶體管理器索取 8 位元組記憶體,實際的記憶體開銷極可能是 12 位元組,乃至更多。如果想做這方面的點竄,讓 lua 的 gc 能更真正的反映記憶體實際使用情形,保舉修改 lmem.c 的 76 行,luaM_realloc_函式。所有的 lua 中的記憶體使用改變城市經由過程這個函式。

從下面這段程式碼中,我們還見到了 GCSWEEPCOST 這個祕密數字。這個數字用於節制 GC 的進度。這超越了本日的話題。留待今後闡發。

接下來就是對所有未標記的別的 GCObject 做清理勞動了。即 GCSsweep 階段。它和上面的 GCSsweepstring 近似。

最終是 GCSfinalize 階段。假如在前方的階段發現了需要呼叫 gc 元方法的 userdata 物件,將在這個階段逐一呼叫。做這件工作的函式是 GCTM

後面已談到過,全部具有 gc 元要領的 userdata 物件和其聯絡關係的資料,現實上都不會在以前的斷根階段被肅清。(因為零丁做過標誌)所有的元方法挪用都是平安的。而它們的現實排除,則需比及下一次 GC 流程了。或是在lua_close被調歷時消滅。

ps.lua_close其實不做完全的 gc 事情,只是簡單的處置懲罰所有 userdata 的 gc 元方法,以及開釋所有效到的記憶體。它是相對於便宜的。

接下來我們看看 GC 標記流程觸及的一些觀點。

簡略的說,lua 以為每一個 GCObject (需要被 GC 蒐集器辦理的工具)都有一個色彩。一起頭,一切節點都是白色的。新建立進去的節點也被默許配置為紅色。

在標記階段,可見的節點,逐個被設定為玄色。有些節點比較龐大,它會聯絡關係此外節點。再不處置完所相關聯節點前,lua 以為它是灰色的。

節點的顏色被貯存在 GCObject 的 CommonHeader 裡,放在 marked 域中。為了節儉記憶體,因此用位寄存。marked 是一個單位元組量。統共可以儲存 8 個標記。而 lua 5.1.4 用到了 7 個標記位。在 lgc.h 的 41 行,有其詮釋:

/*
** Layout for bit use in `marked' field:
** bit 0 - object is white (type 0)
** bit 1 - object is white (type 1)
** bit 2 - object is black
** bit 3 - for userdata: has been finalized
** bit 3 - for tables: has weak keys
** bit 4 - for tables: has weak values
** bit 5 - object is fixed (should not be collected)
** bit 6 - object is "super" fixed (only the main thread)
*/


#define WHITE0BIT   0
#define WHITE1BIT   1
#define BLACKBIT    2
#define FINALIZEDBIT    3
#define KEYWEAKBIT  3
#define VALUEWEAKBIT    4
#define FIXEDBIT    5
#define SFIXEDBIT   6
#define WHITEBITS   bit2mask(WHITE0BIT, WHITE1BIT)

lua 定義了一組巨集來操縱這些標記位,程式碼就再也不列出。只要要翻開 lgc.h 就可以很輕快的明白這些巨集的函式。

白色和黑色是劃分標記的。當一個物件非白非黑時,就認為它是灰色的。

為何有兩個白色符號位?這是 lua 採取的一個小技能。在 GC 的標誌流程完成,但清算流程還沒有作完前。一旦物件間的干係產生轉變,好比新增添了一個物件。這些物件的生命期是不能預知的。最安定的方法是把它們標記為不可消除的。由於清理程序結束,需要把所有物件設定回白色,便利下次清算。lua 其實是單遍掃描,處置完一個節點就重置一個節點的顏色的。簡樸的把新創建出來的物件設定為黑,有可能致使它在 GC 流程竣事後,再也沒機遇變回白色了。

簡略的方法便是設定從第三種狀態。也便是第 2 種白色。

在 Lua 中,兩個白色狀態是一個乒乓開關,當前需要刪除 0 型白色節點時, 1 型白色節點就是被保護起來的;反之也同樣。

以後的白色是 0 型仍是 1 型,見global_State的 currentwhite 域。otherwhite() 用於乒乓切換。獲適當前白色狀態,利用定義在 lgcc.h 77 行的巨集:

#define luaC_white(g)   cast(lu_byte, (g)->currentwhite & WHITEBITS)

FINALIZEDBIT 用於標記 userdata 。當 userdata 確定不被援用,則設定上這個標記。它分歧於顏色標記。因為 userdata 由於 gc 元方法的存在,釋放所佔記憶體是需要放到 gc 元方法呼叫以後的。這個標記可以包管元方法不會被頻頻呼叫。

KEYWEAKBIT 和 VALUEWEAKBIT 用於標記 table 的 weak 屬性,無需多言。

FIXEDBIT 可以保證一個 GCObject 不會在 GC 流程中被清除。為什麼要有這類狀態?關鍵在於 lua 本身會用到一個字串,它們有可能不被任何處所引用,但在屢次用到這個字串時。那麼,這些字串就會被珍愛起來,設定上 FIXEDBIT ,。

在 lstring.h 的 24 行界說有:

#define luaS_fix(s) l_setbit((s)->tsv.marked, FIXEDBIT)

能夠把一個字串配置為被庇護的。

典範的運用場所見 llex.c 的 64 行:

void luaX_init (lua_State *L) {
  int i;
  for (i=0; i<NUM_RESERVED; i++) 
TString *ts="luaS_new(L," luaX_tokens[i]); 
luaS_fix(ts); 
reserved words are never collected * lua_assert(strlen(luaX_tokens[i])+1 <="TOKEN_LEN);" ts->tsv.reserved = cast_byte(i+1);  /* reserved word */
}

以及 ltm.c 的 30 行:

void luaT_init (lua_State *L) {
  static const char *const luaT_eventname[] = /* ORDER TM */
    "__index", "__newindex",
    "__gc", "__mode", "__eq",
    "__add", "__sub", "__mul", "__div", "__mod",
    "__pow", "__unm", "__len", "__lt", "__le",
    "__concat", "__call";
  int i;
  for (i=0; itmname[i] = luaS_new(L, luaT_eventname[i]);
    luaS_fix(G(L)->tmname[i]);  /* never collect these names */
}

以元方式為例,若是咱們操縱 lua 尺度 api 來摹擬 metatable 的行動,就不能寫的和原生的 meta 機制高效。由於,當我們取到一個 table 的 key ,想知道它是否是__index時,要末我們需要挪用 strcmp 做比力;要麼利用lua_pushlstring先將需要比較的 string 壓入lua_State,而後再比較。

咱們曉得 lua 中值分歧的 string 同享了一個 string 工具,即 TString 地點是一概的。對照兩個 lua string 的價格是很小(只需求比力一個指標),比 C 函式 strcmp 高效。但lua_pushlstring卻有額外開消。它必要去計較 hash 值,盤問 hash 表 (string table) 。

lua 的 GC 演算法並不做記憶體清算,它不會在記憶體中遷徙資料。實際上,如果你能確定一個 string 不會被清除,那末它的記憶體地點也是穩定的,這樣就帶來的優化空間。ltm.c 中就是如許做的。

見 lstate.c 的 93 行:

TString *tmname[TM_N];  /* array with tag-method names */

global_State中 tmname 域就乾脆以 TString 指標的體例記實了所有元方法的名字。換作準則的 lua api 來做的話,凡是我們需要把這些 string 放到登錄檔,或情況表中,才幹擔保其不被 gc 清除,且可以在比較時拿到。lua 本身的完成則哄騙 FIXEDBIT 做了一步優化。

最後,我們來看看 SFIXEDBIT 。實在它的用處只要一個,就是標記主 mainthread 。也就是一概的出發點。我們呼叫lua_newstate返回的阿誰佈局。

為甚麼需要把這個佈局特別看待?因為即便到lua_close的那一刻,這個構造也是不克隨便清除的。我們來看看天下末日時,法式都履行了什麼?見 lstate.c 的 105 行。

static void close_state (lua_State *L) global_State *g = G(L);
  luaF_close(L, L->stack);  /* close all upvalues for this thread */
  luaC_freeall(L);  /* collect all objects */
  lua_assert(g->rootgc == obj2gco(L));
  lua_assert(g->strt.nuse == 0);
  luaM_freearray(L, G(L)->strt.hash, G(L)->strt.size, TString *);
  luaZ_freebuffer(L, &g->buff);
  freestack(L, L);
  lua_assert(g->totalbytes == sizeof(LG));
  (*g->frealloc)(g->ud, fromstate(L), state_size(LG), 0);

這是lua_close的最後一個步驟。luaC_freeall將釋放所有的 GCObject ,但不包羅有 SFIXEDBIT 的 mainthread 物件 。見 lgc.c 484 行

void luaC_freeall (lua_State *L) bitmask(SFIXEDBIT);  /* mask to collect all elements */
  sweepwholelist(L, &g->rootgc);
  for (i = 0; i < g->strt.size; i++)  /* free all string lists */
    sweepwholelist(L, &g->strt.hash[i]);

這裡 FIXEDBIT 是被疏忽的,而在此以前,FIXEDBIT 被護衛著。見 lstate.c 的 153 行(lua_newstate函式):

g->currentwhite = bit2mask(WHITE0BIT, FIXEDBIT);

這麼做很輕易瞭解,lua 天下的發源,統統根資料都放在這個物件中,要是被提早清理,背面的程式碼就會出題目。真實釋放這個物件不是在 GC 中,而是末了那句:

lua_assert(g->totalbytes == sizeof(LG));
  (*g->frealloc)(g->ud, fromstate(L), state_size(LG), 0);

順帶還 assert 了一下,最後,世界上是不是隻剩下這個結構。