lua_gc源碼學習一

來源:互聯網
上載者:User

轉自別人的部落格  備用學習

最近發現在大資料量的 lua 環境中,GC 佔據了很多的 CPU
。差不多是整個 CPU 時間的 20% 左右。希望著手改進。這樣,必須先對 lua 的 gc 演算法極其實現有一個詳盡的理解。我之前讀過 lua 的原始碼,由於 lua 源碼版本變遷,這個工作還需要再做一次。這次我重新閱讀了 lua 5.1.4 的原始碼。從今天起,做一個筆記,詳細分析一下 lua 的 gc 是如何?的。閱讀代碼整整花掉了我一天時間。但寫出來恐怕比閱讀時間更長。我會分幾天寫在 blog 上。

Lua 採用一個簡單的標記清除演算法的 GC 系統。

在 Lua 中,一共只有 9 種資料類型,分別為 nil 、boolean 、lightuserdata 、number 、string 、 table 、 function 、 userdata 和 thread 。

其中,只有 string table function thread 四種在 vm 中以引用方式共用,是需要被 GC 管理回收的對象。其它類型都以值形式存在

但在 Lua 的實現中,還有兩種類型的對象需要被 GC 管理。分別是 proto (可以看作未綁定 upvalue 的函數), upvalue (多個 upvalue 會引用同一個值)。

Lua 是以 union + type 的形式儲存值。具體定義可見 lobject.h 的 56 - 75 行:

typedef union 
{ 
GCObject *gc; 
void *p; 
lua_Number n; 
int b; 
} Value; 
 
#define TValuefields Value value; int tt 
 
typedef struct lua_TValue 
{ 
TValuefields; 
} TValue;

我們可以看到,Value 以 union 方式定義。如果是需要被 GC 管理的對象,就以 GCObject 指標形式儲存,否則直接存值

在代碼的其它部分,並不直接使用 Value 類型,而是 TValue 類型。它比 Value 多了一個類型標識。用 int tt 記錄。

通常的系統中,每個 TValue 長為 12 位元組。btw, 在The implementation of Lua 5.0中作者討論了,在 32 位系統下,為何不用某種 trick 把 type 壓縮到前 8 位元組內。

所有的 GCObject 都有一個相同的資料頭,叫作 CommonHeader ,在 lobject.h 裡 43 行以宏形式定義出來的。使用宏是源於使用上的某種便利。C 語言不支援結構的繼承。

#define CommonHeader GCObject *next; lu_byte tt; lu_byte marked
/*GCObject 定義裡擁有CommonHeader 實現了單鏈表結構功能*/

從這裡我們可以看到:所有的 GCObject 都用一個單向鏈表串了起來。每個對象都以 tt 來識別其類型。marked 域用於標記清除的工作。

標記清除演算法是一種簡單的 GC 演算法。每次 GC 過程,先以若干根節點開始,逐個把直接以及間接和它們相關的節點都做上標記。對於 Lua ,這個過程很容易實現。因為所有 GObject 都在同一個鏈表上,當標記完成後,遍曆這個鏈表,把未被標記的節點一一刪除即可。

Lua 在實際實現時,其實不只用一條鏈表維繫所有 GCObject 。這是因為 string 類型有其特殊性。所有的 string 放在一張大的 hash 表中。它需要保證系統中不會有值相同的 string 被建立兩份。顧 string 是被單獨管理的,而不串在 GCObject 的鏈表中。

回頭來看看lua_State這個類型。這是寫 C 和 Lua 互動時用的最多的資料類型。顧名思義,它表示了 lua vm 的某種狀態。從實現上來說,更接近 lua 的一個 thread 以及其間包含的相關資料(堆棧、環境等等)。事實上,一個lua_State也是一個類型為 thread 的 GCObject 。見其定義於 lstate.h 97 行。

struct lua_State 
{ 
CommonHeader;
lu_byte status; 
StkId top; 
StkId base; 
global_State *l_G; 
CallInfo *ci; //當前函數的調用資訊
const Instruction *savedpc;
StkId stack_last; 
StkId stack; 
CallInfo *end_ci; 
CallInfo *base_ci; 
int stacksize; 
int size_ci; 
unsigned short nCcalls; 
unsigned short baseCcalls; 
lu_byte hookmask; 
lu_byte allowhook; 
int basehookcount; 
int hookcount; 
lua_Hook hook; 
TValue l_gt; 
TValue env; 
GCObject *openupval; 
GCObject *gclist; 
struct lua_longjmp *errorJmp; 
ptrdiff_t errfunc; 
};

一個完整的 lua 虛擬機器在運行時,可有多個lua_State,即多個 thread 。它們會共用一些資料。這些資料放在global_State *l_G域中。其中自然也包括所有 GCobject 的鏈表。

所有的 string 則以 stringtable 結構儲存在 stringtable strt 域。string 的實值型別為 TString ,它和其它 GCObject 一樣,擁有 CommonHeader 。但需要注意,CommonHeader 中的 next 域卻和其它類型的單向鏈表意義不同。它被掛接在 stringtable 這個 hash 表中。

除 string 外的 GCObject 鏈表頭在 rootgc ( lstate.h 75 行)域中。初始化時,這個域被初始化為主線程。見 lstate.c 170 行,lua_newstate函數中:

g->rootgc = obj2gco(L);

每當一個新的 GCobject 被建立出來,都會被掛接到這個鏈表上,掛接函數有兩個,在 lgc.c 687 行的

void luaC_link (lua_State *L, GCObject *o, lu_byte tt)
{
    global_State *g = G(L); 
    o->gch.next = g->rootgc; 
    g->rootgc = o; 
    o->gch.marked = luaC_white(g); 
    o->gch.tt = tt; 
} 
 
void luaC_linkupval (lua_State *L, UpVal *uv) 
{ 
     global_State *g = G(L); 
     GCObject *o = obj2gco(uv); 
     o->gch.next = g->rootgc; 
     g->rootgc = o; 
     if (isgray(o)) 
     { 
          if (g->gcstate == GCSpropagate)
             { 
                 gray2black(o); 
                 luaC_barrier(L, uv, uv->v); 
             } 
          else 
          { 
                makewhite(g, o); 
                lua_assert(g->gcstate != GCSfinalize && g->gcstate != GCSpause); 
           }
      }
 }

upvalue 在 C 中類型為 UpVal ,也是一個 GCObject 。但這裡被特殊處理。為什麼會這樣?

因為 Lua 的 GC 可以分步掃描。別的類型被新建立時,都可以直接作為一個白色節點(新節點)掛接在整個系統中。

upvalue 卻是對已有的對象的間接引用,不是新資料。一旦 GC 在 mark 的過程中( gc 狀態為 GCSpropagate ),則需增加屏障luaC_barrier。對於這個問題,會在以後詳細展開。

lua 還有另一種資料類型建立時的掛接過程也被特殊處理。那就是 userdata 。見 lstring.c 的 95 行:

Udata *luaS_newudata (lua_State *L, size_t s, Table *e) 
{ 
Udata *u; 
if (s > MAX_SIZET - sizeof(Udata)) 
luaM_toobig(L);
u = cast(Udata *, luaM_malloc(L, s + sizeof(Udata))); 
u->uv.marked = luaC_white(G(L)); 
u->uv.tt = LUA_TUSERDATA; 
u->uv.len = s; 
u->uv.metatable = NULL; 
u->uv.env = e; 
u->uv.next = G(L)->mainthread->next; 
G(L)->mainthread->next = obj2gco(u); 
return u; 
}

這裡並沒有調用luaC_link來掛接新的 Udata 對象,而是直接使用的

 u->uv.next = G(L)->mainthread->next; G(L)->mainthread->next = obj2gco(u);

把 u 掛接在 mainthread 之後。

從前面的 mainstate 建立過程可知。mainthread 一定是 GCObject 鏈表上的最後一個節點(除 Udata 外)。這是因為掛接過程都是向鏈表頭添加的。

這裡,就可以把所有 userdata 全部掛接在其它類型之後。這麼做的理由是,所有 userdata 都可能有 gc 方法(其它類型則沒有)。需要統一去調用這些 gc 方面,則應該有一個途徑來單獨遍曆所有的 userdata 。除此之外,userdata 和其它 GCObject 的處理方式則沒有區別,顧依舊掛接在整個 GCObject 鏈表上而不需要單獨再分出一個鏈表。

處理 userdata 的流程見 lgc.c 的 127 行

size_t luaC_separateudata (lua_State *L, int all) 
{

這個函數會把所有帶有 gc 方法的 userdata 挑出來,放到一個迴圈鏈表中。這個迴圈鏈表在global_State的 tmudata 域。需要調用 gc 方法的這些 userdata 在當個 gc 迴圈是不能被直接清除的。所以在 mark 環節的最後,會被重新 mark 為不可清除節點。見 lgc.c 的 545 行:

marktmu(g);

這樣,可以保證在調用 gc 方法環節,這些對象的記憶體都沒有被釋放。但因為這些對象被設定了 finalized 標記。(通過 markfinalized ),下一次 gc 過程不會進入 tmudata 鏈表,將會被正確清理。

具體 userdata 的清理流程,會在後面展開解釋。

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.