lua源碼分析
-
[x] 數據類型
-
數據類型的分類
#define LUA_TNONE (-1) //空類型 #define LUA_TNIL 0 //nil #define LUA_TBOOLEAN 1 //布爾 #define LUA_TLIGHTUSERDATA 2//指針 #define LUA_TNUMBER 3//數字 #define LUA_TSTRING 4//字符串 #define LUA_TTABLE 5//表 #define LUA_TFUNCTION 6//函數 #define LUA_TUSERDATA 7//指針 #define LUA_TTHREAD 8//虛擬機,攜程 //可以看到除了LUA_TNONE,lua支持了9種基本類型。其實對於某些基本類型,lua還用了一個叫做variant tag的標記來定義其子類型,具體參見下圖: /* Variant tags for functions */ #define LUA_TLCL (LUA_TFUNCTION | (0 << 4)) /* Lua closure (對於lua函數來說沒有函數這個概念,所有的lua函數都是一個lua閉包)*/ #define LUA_TLCF (LUA_TFUNCTION | (1 << 4)) /* light C function (C函數) */ #define LUA_TCCL (LUA_TFUNCTION | (2 << 4)) /* C closure (c閉包)*/ /* Variant tags for strings */ #define LUA_TSHRSTR (LUA_TSTRING | (0 << 4)) /* short strings */ #define LUA_TLNGSTR (LUA_TSTRING | (1 << 4)) /* long strings */ /* Variant tags for numbers */ #define LUA_TNUMFLT (LUA_TNUMBER | (0 << 4)) /* float numbers */ #define LUA_TNUMINT (LUA_TNUMBER | (1 << 4)) /* integer numbers */ //string又細分為短string和長string,主要區別在於它們的hash值生成;而number也細分為了float和integer。
-
Type的組織方式
-
Value和TValue
首先,lua為了方便對所有的類型進行統一管理,把它們都抽象成了一個叫做Value的union結構,具體定義如下
/* ** Tagged Values. This is the basic representation of values in Lua, ** an actual value plus a tag with its type. */ /* ** Union of all Lua values */ typedef union Value { GCObject *gc; /* collectable objects */ void *p; /* light userdata */ int b; /* booleans */ lua_CFunction f; /* light C functions */ lua_Integer i; /* integer numbers */ lua_Number n; /* float numbers */ } Value; /* 從定義可以看出,主要把這些類型划分為了需要GC的類型和不需要GC的類型。由於Value是union的結構,所以每個Value實例里同時只會有一個字段是有效的。而為了知道具體哪個字段是有效的,也就是具體該Value是什么類型,從而有了TValue這個struct結構,主要在Value基礎上wrap了一個_tt字段來標識Value的具體類型。TValue的定義如下:*/ #define TValuefields Value value_; int tt_ typedef struct lua_TValue { TValuefields; } TValue;
-
GCUnion、GCObject、CommonHeader
lua把所有值按是否需要被GC,划分為了GCObject和一般類型。所有需要被GC的類型,被定義在了GCUnion里:
/* ** Union of all collectable objects (only for conversions) */ union GCUnion { GCObject gc; /* common header */ struct TString ts; struct Udata u; union Closure cl; struct Table h; struct Proto p; struct lua_State th; /* thread */ }; /*可以發現String、UserData、Closure、Table、Proto、luaState等類型都是需要被GC的,GCUnion結構和Value類似,也是同時只有一個字段是有效的。所以我們自然而然會想到,是不是類似TValue一樣,在外面給包一層type呢,但是lua實現這邊並沒有這樣做,而是讓TString、UData這些"子類"都在各自定義的開頭定義這個type字段。實際上,是定義了一個叫做CommonHeader的宏,這個宏里包含了type和一些其他字段,而每個GC類型都需要在在其struct頭部定義該宏,從而可以造成一種所有GC類型都繼承自一個帶有CommonHeader宏的基類的假象。該宏定義如下:*/ #define CommonHeader GCObject *next; lu_byte tt; lu_byte marked struct GCObject { CommonHeader; }; /* 可以發現,它一共有三個字段: tt,即該GC對象的具體類型 next,指向GCObject的指針,用於GC算法內部實現鏈表 marked,用於GC算法內部實現 GCObject,其實它就是把CommonHeader這個數據區包成了一個struct,它的好處在於lua可以把所有的GC類型的對象都視作是一個GCObject,比如在lua_State結構里就定義了一個GC列表: GCObject* gclist。 */
-
-
-
[x] 字符串
-
長串和短串
在講String的數據結構和重要函數前,先強調一點,出於對性能和內存等方面的考慮,lua對String實現的方方面面,都把短字符串和長字符串區分開來處理了。比如短串會走一個先查詢再創建的路子,而長串不查詢,直接創建。我個人的理解大概是出於以下兩個方面考慮:
- 復用度。短串復用度會比長串要高,或者說短串在全局的引用計數一般會比長串高。比如obj["id"] = 12, obj["type"] = 0,類似"id"、"type"這種短串可能會在程序很多處地方使用到,如果開辟多份就有點浪費了。而長串則很少會有重復的,比如我一篇文章的文本,一般不會在兩個地方重復打出來。
- 哈希效率。由於長串的字符串比較多,如果要把組成它的字符序列進行哈希,耗時會比短串長。
- 從定義可以看出,長度大於40的,在lua中處理為長串,反之則為短串。
-
String的基本數據結構
-
lua中string的實現為TString結構體:
typedef struct TString { CommonHeader; //gc用結構體 lu_byte extra; /* 對於短串,主要用於實現保留字符串;對於長串,作為一個標識,來表示該串有沒有進行過hash,這點可以結合hash字段來理解。*/ lu_byte shrlen; /* 短串的長度,對於長串沒有意義。 */ unsigned int hash;/* 該字符串的hash值,如果是短串,該hash值是在創建時就計算出來的,這是因為短串會加入到全局的stringtable這個hashmap結構中;而對於長串來說,這個hash字段是按需的,只有真正需要它的hash值時,手動調用luaS_hashlongstr函數,才會生成該值,lua內部現在只有在把長串作為table的key時,才會去計算它。當extra字段為0時,表示該長串的hash還沒計算過,否則表示已經計算過了 */ union { /* 當是短串時,由於會被加入到全局stringtable的鏈表中,所以在該結構中保存了指向下一個TString的指針;當是長串時,表示該長串的長度。注意長串和短串沒有共用一個字段來表示它們的長度,主要是長串的長度可以很長,而短串最長就為40,一個byte就夠用了,這邊也體現了lua實現是很摳細節的,反倒是把這兩個不相關的字段打包到一個union里來節約內存了。 */ size_t lnglen; /* length for long strings */ struct TString *hnext; /* linked list for hash table */ } u; } TString;
-
stringtable
typedef struct stringtable { TString **hash;//基於TString的hashmap,也叫做散列桶。基本結構是一個數組,每個數組里存的是相同hash值的TString的鏈表。 int nuse; /* 當前實際的元素數 */ int size; /* 桶個數 */ } stringtable;
-
-
string實現中的重要函數
-
luaS_newlstr函數
/*創建給定長度的string接口,由於lua內部對短串和長串的處理采用了兩套方案,所以該函數會根據string的實際長度來決定調用短串處理函數(internshrstr)還是長串處理函數(luaS_createlngstrobj)。*/ TString *luaS_newlstr (lua_State *L, const char *str, size_t l) { if (l <= LUAI_MAXSHORTLEN) /* short string? */ return internshrstr(L, str, l); else { TString *ts; if (l >= (MAX_SIZE - sizeof(TString))/sizeof(char)) luaM_toobig(L); ts = luaS_createlngstrobj(L, l); memcpy(getstr(ts), str, l * sizeof(char)); return ts; } }
-
internshrstr函數
static TString *internshrstr (lua_State *L, const char *str, size_t l) { TString *ts; global_State *g = G(L); /* hash查找。會先調用luaS_hash來得到字符串的hash值,然后去全局stringtable里查找,如果有就直接返回該TString對象。hash的具體實現可以參照luaS_hash函數,為了對長度較長的字符串不逐位hash,內部也是根據長度的2次冪計算出了一個步長step,來加速hash的過程。 */ unsigned int h = luaS_hash(str, l, g->seed); TString **list = &g->strt.hash[lmod(h, g->strt.size)]; lua_assert(str != NULL); /* otherwise 'memcmp'/'memcpy' are undefined */ for (ts = *list; ts != NULL; ts = ts->u.hnext) { if (l == ts->shrlen && (memcmp(str, getstr(ts), l * sizeof(char)) == 0)) { /* found! */ if (isdead(g, ts)) /* dead (but not collected yet)? */ changewhite(ts); /* resurrect it */ return ts; } } /* hashtable按需resize。如果第一步查找失敗了,並且stringtable的元素數量已經大於桶數,那么以兩倍的尺寸對stringtable進行resize。具體調用的是luaS_resize函數。 */ if (g->strt.nuse >= g->strt.size && g->strt.size <= MAX_INT/2) { luaS_resize(L, g->strt.size * 2); list = &g->strt.hash[lmod(h, g->strt.size)]; /* recompute with new size */ } /* 實際的TString創建工作。主要是調用createstrobj函數來實現。其中包括了內存的分配,CommonHeader的填充,TString特化字段的填充等。 */ ts = createstrobj(L, l, LUA_TSHRSTR, h); memcpy(getstr(ts), str, l * sizeof(char)); ts->shrlen = cast_byte(l); ts->u.hnext = *list; *list = ts; g->strt.nuse++; /* 更新stringtable信息。比如更新stringtable的鏈表,以及對stringtable的元素數量加1。 */ return ts; }
-
luaS_reisze函數
/* 實際改變stringtable桶數量的函數。它目前只會被兩個地方調用到: 1.短string創建時(internshrstr函數),如果發現桶數量小於了元素數量,說明散列比較擁擠了,會對桶進行兩倍的擴容。 2.在gc時,如果發現桶數量大於了4倍的元素數量,說明散列太稀疏了,會對桶數量進行減半操作。 */ void luaS_resize (lua_State *L, int newsize) { int i; stringtable *tb = &G(L)->strt; /* newsize>oldsize。這個時候的順序是,先進行擴容,然后進行rehash。擴容跟到里面去調用的就是realloc函數。 而rehash的代碼也很簡潔,就是簡單的遍歷每個桶,把每個桶里的元素再哈希到正確的桶里去 */ if (newsize > tb->size) { /* grow table if needed */ luaM_reallocvector(L, tb->hash, tb->size, newsize, TString *); for (i = tb->size; i < newsize; i++) tb->hash[i] = NULL; } for (i = 0; i < tb->size; i++) { /* rehash */ TString *p = tb->hash[i]; tb->hash[i] = NULL; while (p) { /* for each node in the list */ TString *hnext = p->u.hnext; /* save next */ unsigned int h = lmod(p->hash, newsize); /* new position */ p->u.hnext = tb->hash[h]; /* chain it */ tb->hash[h] = p; p = hnext; } } /* newsize < oldsize。順序是倒過來的,需要先根據newsize進行rehash,然后在保證所有元素已經收縮到newsize個數的桶里以后,才能進行shrink操作,這里也是調用的realloc函數來實現。 */ if (newsize < tb->size) { /* shrink table if needed */ /* vanishing slice should be empty */ lua_assert(tb->hash[newsize] == NULL && tb->hash[tb->size - 1] == NULL); luaM_reallocvector(L, tb->hash, tb->size, newsize, TString *); } tb->size = newsize; } /* luaM_realloc_速覽, frealloc(ud, NULL, x, s) 可以創建一個新的內存塊,大小為s frealloc(ud, p, x, 0) 釋放內存塊b 底層調用(*g->frealloc),lua就是普通malloc,如果想設計的話可以嘗試類似gunc的pool malloc或者bit map malloc做緩存池,我后面有空來試試,通過lua_setallocf接口可以設置這個frealloc函數 */ void *luaM_realloc_ (lua_State *L, void *block, size_t osize, size_t nsize) { void *newblock; global_State *g = G(L); size_t realosize = (block) ? osize : 0; lua_assert((realosize == 0) == (block == NULL)); #if defined(HARDMEMTESTS) if (nsize > realosize && g->gcrunning) luaC_fullgc(L, 1); /* force a GC whenever possible */ #endif newblock = (*g->frealloc)(g->ud, block, osize, nsize); if (newblock == NULL && nsize > 0) { lua_assert(nsize > realosize); /* cannot fail when shrinking a block */ if (g->version) { /* is state fully built? */ luaC_fullgc(L, 1); /* try to free some memory... */ newblock = (*g->frealloc)(g->ud, block, osize, nsize); /* try again */ } if (newblock == NULL) luaD_throw(L, LUA_ERRMEM); } lua_assert((nsize == 0) == (newblock == NULL)); g->GCdebt = (g->GCdebt + nsize) - realosize; return newblock; }
-
createstrobj函數
static TString *createstrobj (lua_State *L, size_t l, int tag, unsigned int h) { TString *ts; GCObject *o; size_t totalsize; /* total size of TString object */ totalsize = sizelstring(l);//計算該string需要占用的內存大小size。lua實際把string的char數組緊貼UTString結構來存儲,所以一個string實例實際占用內存大小其實是UTString結構占用,再加上(charlength+1)個char大小: o = luaC_newobj(L, tag, totalsize);//調用luaC_newobj來創建一個GC對象,該函數也是lua中相當重要的函數了。它負責了所有GCObject子類的創建,它會根據實際傳入的內存大小來開辟空間,並幫忙填充掉CommonHeader的數據。最后,它還會把該obj掛接到global_State中的allgc列表中,以供GC模塊使用。 ts = gco2ts(o); ts->hash = h; ts->extra = 0; getstr(ts)[l] = '\0'; //填充對象的TString特化字段。並且給char數組末尾添'\0'。 return ts; }
-
-
總結
在理完了string牽涉的主要數據結構和函數以后。從整個宏觀的情況來理解lua的string:
- 首先,它是一個GCObject的子類,也就意味着它是被垃圾回收模塊所管理的。
- 其次,它的長度決定了它的內部實現機制:短串意味着hashtable管理和內存去重;長串意味着內存副本。
- 最后,全局的hashtable大小,取決於string的創建和gc的sizecheck檢查。
-
-
[x] 虛擬機
- 虛擬機基本概念
- 虛擬機指借助軟件系統對物理機器指令執行進行的一種模擬。首先,對於物理機器的執行,主要是機器從內存中fetch指令,通過總線傳輸到CPU,然后進行譯碼、執行、結果存儲等步驟。既然虛擬機是對其進行的一種模擬,那么也逃不過以下幾個特點:
- 將源碼編譯成VM所能執行的字節碼。
- 字節碼格式(指令格式),例如三元式、四元式、波蘭式等。
- 函數調用的相關棧結構,函數的出入口和傳參方式。
- 指令指針,類似於物理機的指令寄存器(EIP)。
- 虛擬CPU。 instruction dispatche
- 取指:通過IP fetch下一條指令
- 譯碼:對指令進行翻譯,得到指令類型,並且解析其操作數。
- 執行:跳到對應邏輯塊進行執行。
- 虛擬機基本概念
-
-
棧式虛擬機和寄存器式虛擬機
-
棧式虛擬機
-
采用棧式虛擬機的語言有JVM、CPython以及.Net CLR等。
-
基於棧的虛擬機有一個操作數棧的概念,虛擬機在進行真正的運算時都是直接與操作數棧(operand stack)進行交互,不能直接操作內存中數據(其實這句話不嚴謹的,虛擬機的操作數棧也是布局在內存上的),也就是說不管進行何種操作都要通過操作數棧來進行,即使是數據傳遞這種簡單的操作。這樣做的直接好處就是虛擬機可以無視具體的物理架構,特別是寄存器。但缺點也顯而易見,就是速度慢,因為無論什么操作都要通過操作數棧這一結構。:
-
-
寄存器式虛擬機
- 采用寄存器式的虛擬機有lua和Dalvik等。
- 這種實現沒有操作數棧這一概念,但是會有許多的虛擬寄存器。這類虛擬寄存器有別於CPU的寄存器,因為CPU寄存器往往是定址的(比如DX本身就是能存東西),而寄存器式的虛擬機中的寄存器通常有兩層含義:
- 寄存器別名(比如lua里的RA、RB、RC、RBx等),它們往往只是起到一個地址映射的功能,它會根據指令中跟操作數相關的字段計算出操作數實際的內存地址,從而取出操作數進行計算;
- 實際寄存器,有點類似操作數棧,也是一個全局的運行時棧,只不過這個棧是跟函數走的,一個函數對應一個棧幀,棧幀里每個slot就是一個寄存器,第1步中通過別名映射后的地址就是每個slot的地址。具體的棧幀可以參考后文講CallInfo時的棧幀圖。 好處是指令條數少,數據轉移次數少。壞處是單挑指令長度較長。
- 具體來看,lua里的實際寄存器數組是用TValue結構的棧來模擬的,這個棧也是lua和C進行交互的虛擬棧。 lua里的字節碼叫做opcode,
-
-
關鍵函數和結構分析
-
luaL_dofile
//包含了luaL_loadfile和lua_pcall兩個步驟,分別對應了函數的解析和執行階段。 #define luaL_dofile(L, fn) \ (luaL_loadfile(L, fn) || lua_pcall(L, 0, LUA_MULTRET, 0))
-
luaL_loadfile
#define luaL_loadfile(L,f) luaL_loadfilex(L,f,NULL) //再跟進去核心是會調用到lua_load LUA_API int lua_load (lua_State *L, lua_Reader reader, void *data, const char *chunkname, const char *mode) { ZIO z; int status; lua_lock(L); if (!chunkname) chunkname = "?"; luaZ_init(L, &z, reader, data); status = luaD_protectedparser(L, &z, chunkname, mode);//這句話是重點,對lua代碼進行進行詞法和語法分析,把source轉化成opcode,並創建Proto結構保存該opcode和該函數的元信息 if (status == LUA_OK) { /* no errors? */ LClosure *f = clLvalue(L->top - 1); /* get newly created function */ if (f->nupvalues >= 1) { /* does it have an upvalue? */ /* get global table from registry */ Table *reg = hvalue(&G(L)->l_registry); const TValue *gt = luaH_getint(reg, LUA_RIDX_GLOBALS); /* set global table as 1st upvalue of 'f' (may be LUA_ENV) */ setobj(L, f->upvals[0]->v, gt); luaC_upvalbarrier(L, f->upvals[0]); } } lua_unlock(L); return status; } //---快進n步到closefunc函數里面就初始化了Proto結構和信息 static void close_func (LexState *ls) { lua_State *L = ls->L; FuncState *fs = ls->fs; Proto *f = fs->f; luaK_ret(fs, 0, 0); /* final return */ leaveblock(fs); luaM_reallocvector(L, f->code, f->sizecode, fs->pc, Instruction); f->sizecode = fs->pc; luaM_reallocvector(L, f->lineinfo, f->sizelineinfo, fs->pc, int); f->sizelineinfo = fs->pc; luaM_reallocvector(L, f->k, f->sizek, fs->nk, TValue); f->sizek = fs->nk; luaM_reallocvector(L, f->p, f->sizep, fs->np, Proto *); f->sizep = fs->np; luaM_reallocvector(L, f->locvars, f->sizelocvars, fs->nlocvars, LocVar); f->sizelocvars = fs->nlocvars; luaM_reallocvector(L, f->upvalues, f->sizeupvalues, fs->nups, Upvaldesc); f->sizeupvalues = fs->nups; lua_assert(fs->bl == NULL); ls->fs = fs->prev; luaC_checkGC(L); } // Proto結構如下: typedef struct Proto { CommonHeader; lu_byte numparams; /* number of fixed parameters */ lu_byte is_vararg; lu_byte maxstacksize; /* number of registers needed by this function */ int sizeupvalues; /* size of 'upvalues' */ int sizek; /* size of 'k' */ int sizecode; int sizelineinfo; int sizep; /* size of 'p' */ int sizelocvars; int linedefined; /* debug information */ int lastlinedefined; /* debug information */ TValue *k; /* constants used by the function */ Instruction *code; /* opcodes */ struct Proto **p; /* functions defined inside the function */ int *lineinfo; /* map from opcodes to source lines (debug information) */ LocVar *locvars; /* information about local variables (debug information) */ Upvaldesc *upvalues; /* upvalue information */ struct LClosure *cache; /* last-created closure with this prototype */ TString *source; /* used for debug information */ GCObject *gclist; } Proto; /* 該結構基本涵蓋了parse階段該函數的所有分析信息。主要包括以下幾部分: 常量表。比如在函數里寫了a = 1 + 2,那這里的1和2就會放在常量表里。 局部變量信息。包含了局部變量的名字和它在函數中的生存周期區間(用pc來衡量)。 Upvalue信息。包含了該upvalue的名字和它是否歸屬於本函數棧還是外層函數棧的標記。 opcode列表。包含了該函數實際調用的所有指令。其實就是一個int32類型的列表,因為lua虛擬機里每個指令對應一個int32. */
-
lua_pcall
這個函數最終會調到luaD_call,也就是lua虛擬機里函數執行的主要函數。
void luaD_call (lua_State *L, StkId func, int nResults) { if (++L->nCcalls >= LUAI_MAXCCALLS) stackerror(L); if (!luaD_precall(L, func, nResults)) /* is a Lua function? */ luaV_execute(L); /* call it */ L->nCcalls--; }
-
luaD_precall:
- 如果是C函數或者C閉包,會直接創建單個函數調用的運行時結構CallInfo,來完成函數的進棧和出棧。
- 如果是lua閉包,在precall中只會做函數調用前的准備工作,實際執行會在后一步luaV_execute中進行。這里的准備工作主要包括:(1)處理lua的不定長參數、參數數量不夠時的nil填充等。(2)分配CallInfo結構,並填充該函數運行時所需的base、top、opcode等信息,注意CallInfo結構里還有個很關鍵的func字段,它指向棧里對應的LClosure結構,這個結構為虛擬機后續執行提供upvalue表和常量表的查詢,畢竟后續對常量和upvalue的read操作,都是需要把它們從這兩個表中加載到寄存器里的。
-
luaV_execute
- 這一步就是我們前面提到的lua虛擬機的CPU了,因為所有指令的實際執行都是在這個函數里完成的。它做的主要工作,就是在一個大循環里,不斷的fetch和dispatch指令。每次的fetch就是把pc加1,而dispatch就是一個大的swtich-case,每個不同類型的opcode對應不同的執行邏輯。
-
CallInfo
//CallInfo結構,包含了單個函數調用,lua虛擬機所需要的輔助數據結構,它的結構如下: typedef struct CallInfo { StkId func; /* function index in the stack */ StkId top; /* top for this function */ struct CallInfo *previous, *next; /* dynamic call link */ union { struct { /* only for Lua functions */ StkId base; /* base for this function */ const Instruction *savedpc; } l; struct { /* only for C functions */ lua_KFunction k; /* continuation in case of yields */ ptrdiff_t old_errfunc; lua_KContext ctx; /* context info. in case of yields */ } c; } u; ptrdiff_t extra; short nresults; /* expected number of results from this function */ unsigned short callstatus; } CallInfo; /* 我們來看下lua_State里與之相關的幾個字段: stack。TValue*類型,記錄了"內存"起始地址。 base。TValue*類型,記錄當前函數的第一個參數位置。 top。TValue*類型,記錄當前函數的棧頂。 base_ci。當前棧里所有的函數調用CallInfo數組。 ci。當前函數的CallInfo。 可以發現,通過這樣的組織結構,luavm可以方便的獲取到任意函數的位置以及其中的所有參數位置。而每個CallInfo里又記錄了函數的執行pc,因此vm對函數的執行可以說是了如指掌了。 */
-
-
-
[x] Table
-
Table的設計特點
- 容器功能:與其他語言相似,lua也內置了容器功能,也就是table。而與其他語言不同的是,lua內置容器只有table。正因為如此,為了適配不同的應用需求,table的內部結構也比較考究,分為了數組和哈希表兩個部分,根據不同需求來決定使用哪個部分。
- 面向對象功能:與其他語言不同的時,lua並沒有把面向對象的功能以語法的形式包裝給開發者。而是保留了這樣一種能力,待開發者去實現自己的面向對象。而這一保留的能力,也是封裝在table里的:table里可以組合一個metatable,這個metatable本身也是一個table,它的字段用來描述原table的行為。
-
Table的數據結構
typedef struct Table { CommonHeader;//垃圾回收通用結構 lu_byte flags; /*用於cache該表中實現了哪些元方法 */ lu_byte lsizenode; /* 哈希表大小取log2(哈希表大小只會為2的n次冪) */ unsigned int sizearray; /* 數組大小(數組大小只會為2的n次冪) */ TValue *array; /* 數組頭指針 */ Node *node;//哈希表頭指針 Node *lastfree; /* 哈希表可用尾指針,可用的節點只會小於該lastfree節點。 */ struct Table *metatable;//元表 GCObject *gclist;//GC的鏈表,用於垃圾回收 } Table;
-
Table的重要操作
-
查詢操作
/* 查詢key是否存在,分為了int和非int類型。如果key是int類型並且小於sizearray,那么直接返回對應slot。 否則走hash表查詢該key對應的slot。 */ const TValue *luaH_get (Table *t, const TValue *key) { switch (ttype(key)) { case LUA_TSHRSTR: return luaH_getshortstr(t, tsvalue(key)); case LUA_TNUMINT: return luaH_getint(t, ivalue(key));/* integer numbers */ case LUA_TNIL: return luaO_nilobject; case LUA_TNUMFLT: {/* float numbers */ lua_Integer k; if (luaV_tointeger(key, &k, 0)) /* index is int? */ return luaH_getint(t, k); /* use specialized version */ /* else... */ } /* FALLTHROUGH */ default: return getgeneric(t, key); } }
-
新增元素
//由於lua對於下標超過數組的大小的數字,都會存儲在散列表部分去,所以數組部分的插值不會觸發rehash //散列表的組織,就是多個mainposition,每個單獨的mainposition會對應一個數據鏈表,當插入一個key的時候,會調用luaHset\luaH_setnum\luaH_setstr,來獲得該key對應的TValue指針,如果沒有,則調用內部的newkey函數來分配一個新的key TValue *luaH_newkey (lua_State *L, Table *t, const TValue *key) { Node *mp; TValue aux; //...安全性校驗,略 mp = mainposition(t, key);//根據key尋找再當前hash中的位置 if (!ttisnil(gval(mp)) || isdummy(t)) { /*如果該位置已經有數據了(ttisnil(gval(mp)))或者找不到該位置 (isdummy(t))*/ Node *othern; Node *f = getfreepos(t); /* 嘗試獲取一個空閑位置 */ if (f == NULL) { //如果freepos為NULL(沒有可用空間了),調用rehash來擴容。rehash的具體細節將在后文詳解。 rehash(L, t, key); /* grow table */ return luaH_set(L, t, key); /* insert key into grown table */ } lua_assert(!isdummy(t)); //找到newkey的mainposition,看是否可用,如果可用直接使用 othern = mainposition(t, gkey(mp)); if (othern != mp) { //如果占用節點的hash值與newkey不同,說明該節點是被“擠”到該位置來的,那么把該節點挪到freepos去,然后讓newkey入住其mainposition。 while (othern + gnext(othern) != mp) /* find previous */ othern += gnext(othern); gnext(othern) = cast_int(f - othern); /* rechain to point to 'f' */ *f = *mp; /* copy colliding node into free pos. (mp->next also goes) */ if (gnext(mp) != 0) { gnext(f) += cast_int(mp - f); /* correct 'next' */ gnext(mp) = 0; /* now 'mp' is free */ } setnilvalue(gval(mp)); } else { //占用的節點和newkey的哈希值相同,那么直接插入到該mainposition的next。 if (gnext(mp) != 0) gnext(f) = cast_int((mp + gnext(mp)) - f); /* chain new position */ else lua_assert(gnext(f) == 0); gnext(mp) = cast_int(f - mp); mp = f; } } setnodekey(L, &mp->i_key, key); luaC_barrierback(L, t, key); lua_assert(ttisnil(gval(mp))); return gval(mp); }
-
rehash操作:
static void rehash (lua_State *L, Table *t, const TValue *ek) { unsigned int asize; /* 最終數組的大小(一定為2的次冪)。 */ unsigned int na; /* 最終歸入數組部分的key的個數。 */ unsigned int nums[MAXABITS + 1];//它的第i個位置存儲的是key在2^(i-1)~2^i區間內的數量。 int i; int totaluse;//總共的key個數。 for (i = 0; i <= MAXABITS; i++) nums[i] = 0; /* reset counts */ na = numusearray(t, nums); //遍歷當前的array部分,按其中key的分布來更新nums數組。同時返回na。 totaluse = na; /* 將totaluse加上na。 */ totaluse += numusehash(t, nums, &na); /* 遍歷當前的hash表部分,如果其中的key為整數,na++並且更新nums數組,對於每個遍歷的元素,totaluse++ */ na += countint(ek, nums);/* - countint:將newkey傳進去,如果是整型的,那么na++。 - */ totaluse++; asize = computesizes(nums, &na);//計算optimal的array部分大小。這個函數根據整型key在2^(i-1)~2^i之間的填充率,來決定最終的array大小。一旦遇到某個子區間的填充率小於1/2,那么后續的整型key都存儲到hash表中去,這一步是為了防止數組過於稀疏而浪費內存。函數將返回asize以及na luaH_resize(L, t, asize, totaluse - na);//根據上一步計算出的最終數組和哈希表大小,進行resize操作。當然,如果hash表的尺寸有變化,會對原來哈希表中的元素進行真正的rehash。 }
-
迭代操作
//在使用測主要是ipairs和pairs兩個函數。這兩個函數都會在vm內部臨時創建出兩個變量state和index,用於對lua表進行迭代訪問,每次訪問的時候,會調用luaH_next函數 //該函數首先會根據findindex函數,找出迭代器對應lua表的哪個部分,是數組部分還是hash表部分。如果是數組部分,index會小於sizearray,否則會大於sizearray。注意該函數中的兩個循環,只會進一個,取決於搜尋出來的key的index是否小於sizearray。 int luaH_next (lua_State *L, Table *t, StkId key) { unsigned int i = findindex(L, t, key); /* find original element */ for (; i < t->sizearray; i++) { /* try first array part */ if (!ttisnil(&t->array[i])) { /* a non-nil value? */ setivalue(key, i + 1); setobj2s(L, key+1, &t->array[i]); return 1; } } for (i -= t->sizearray; cast_int(i) < sizenode(t); i++) { /* hash part */ if (!ttisnil(gval(gnode(t, i)))) { /* a non-nil value? */ setobj2s(L, key, gkey(gnode(t, i))); setobj2s(L, key+1, gval(gnode(t, i))); return 1; } } return 0; /* no more elements */ }
-
-
-
-
[x] 函數
-
函數閉包
首先,所有的lua函數,都是一個函數閉包。函數閉包是目前主流語言幾乎都支持的一個機制,它指的是一個內部結構,該結構存儲了函數本身以及一個在詞法上包圍該函數的環境,該環境包含了函數外圍作用域的局部變量,通常這些局部變量又稱作upvalue。
#define ClosureHeader \ CommonHeader; lu_byte nupvalues; GCObject *gclist typedef struct CClosure { ClosureHeader; lua_CFunction f; TValue upvalue[1]; /* list of upvalues */ } CClosure; typedef struct LClosure { ClosureHeader;//ClosureHeader:跟GC相關的結構,因為函數與是參與GC的。 struct Proto *p;//因為Closure=函數+upvalue嘛,所以p封裝的就是純粹的函數原型。該結構中封裝了函數的很多基本特性,如局部變量、字節碼序列、函數嵌套、常量表等。 UpVal *upvals[1]; /* upvals:函數的upvalue指針列表,記錄了該函數引用的所有upvals。正是由於該字段的存在,導致函數對upvalue的訪問要快於從全局表_G中向下查找。函數對upvalue的訪問,一般就2個步驟:(1)從closure的upvals數組中按索引號取出upvalue。(2)將upvalue加到luastate的stack中 */ } LClosure; typedef union Closure { CClosure c; LClosure l; } Closure;
-
其他參見虛擬機的proto部分
-
-
-
-
[x] GC
-
基本原理
-
和C#、Java類似,lua采用了Mark&Sweep的算法來進行垃圾回收,與之相對的還有個常用算法是Automatic Reference Counting(ARC)。Mark&Sweep的優點在於不用像ARC在每次賦值操作去操作引用計數(對於動態語言效率有較大影響),也不會有環形引用的問題,但是由於Mark&Sweep在觸發時,需要從root節點全量遍歷被引用到的節點,通常是比較耗時的操作。
在lua5.1之前,lua采用了雙色的mark&sweep,它的大致流程如下:
- mark階段
- 從根節點出發,先將root置黑
- 將root節點直接引用的節點置黑
- 遞歸將黑色節點引用到的節點置黑,直到不再有新增的黑色節點
- sweep階段:
- 仍為白色的節點視為沒有引用,從而將作為垃圾進行回收
該算法是一個不可中斷的同步過程,非常容易造成CPU突刺。針對這個問題,lua從5.1以后便引入了增量GC的實現,將這樣一整個同步的回收cycle,均攤到很多個可以增量執行的分步上,從而達到降低CPU突刺的目的。
- mark階段
-
-
增量Collector與Program的關系
增量Collector與程序是交叉運行的,GC執行完一個step,就會把控制權交還給上層lua程序繼續執行,上層執行一段時間又會觸發下一個GC的step。基於這個運行時間線,Collector需要考慮的問題是:
1、如何將一整個Mark&Sweep的cycle切分成邏輯上的子步驟。
2、如何為每一個GC Step分配合理的工作量,使得一方面GC的頻率可以滿足應用程序對內存的需求,另一方面還要讓每一個GC子步驟不至於造成應用程序的CPU突刺。3、如何解決在兩個GC Step間,Mutator會改變GC狀態的問題
-
GC狀態機
-
針對問題(1),lua的做法是,使用了一個線性的狀態機,將一整個Mark&Sweep的cycle拆分成若干子狀態,每個子狀態下去執行對應的action:
比如pause狀態負責mark roots;propagate狀態負責進行mark狀態的傳播,最終mark出所有reachable的節點;幾個swp***狀態負責清理unreachable節點的內存;callfin負責調用對象的finalizer。但是有的狀態也是不可拆分的,具體情況具體分析。
-
GC Step工作量評估
-
如果只是以3.1模型中的以單個子狀態為一個GC Step進行增量GC,實際上效果並不理想。這是因為:
- 就算每個狀態的工作量是相等的,那每個狀態的執行時間也只是整個cycle的1/8,而一般整個cycle的時間,拿手游項目為例,通常為幾十ms幾百ms不等,均攤到每個狀態的耗時,也仍然有幾ms幾十ms,仍然會造成明顯的CPU突刺。
- 更壞的情況是,各個子狀態的耗時通常是非常不均等的,像propagate這種狀態,要去全量遞歸的尋找reachable的節點,往往是一個耗時的大頭,這樣會導致在這個狀態的耗時會非常突出。
基於以上兩個考慮,可以得到的結論是,不能僅僅以GC的子狀態為增量的最小單元。
lua為了解決這個問題,引入了一個“打工還債”的模型:把內存申請(如newstring,newtable等)量化成增加債務,當欠債達到一定規模時,程序運行會從Program態轉變為Collector態,Collector會去打工還債,打工的內容就是上圖中各個子狀態下的action。為了進一步細化增量的最小單位,lua進一步對打工的工作量進行了評估,比如在propagate狀態下,工作量就是traverse對象的內存量。有了這個債務與工作量的量化指標,那lua就可以把增量的單位進一步細化到工作量上
-
三色Mark&Sweep算法
-
對於問題(3)-如何解決在兩個Step之間,Mutator會改變GC狀態的問題。lua采用了下圖的3色Mark&Sweep模型來解決:
該算法的主要步驟還是mark和sweep兩個大階段。mark負責標記當前正在被程序使用的obj,sweep階段負責釋放沒有被程序使用的obj。
相對於lua5.1之前的GC模型,該模型的主要特點是:
1.把GC的obj划分成3個顏色。- 黑色節點:已經完全掃描過的obj。
- 灰色節點:在掃描黑色節點時候初步掃描到,但是還未完全掃描的obj,這類obj會被放到一個待處理列表中進行逐個完全掃描。
- 白色節點:還未被任何黑色節點所引用的obj(因為一旦被黑色節點引用將被置為黑色或灰色)。這里白色又被進一步細分為cur white和old white,lua會記錄當前的cur white顏色,每個obj新創建的時候都是cur white,lua會在mark階段結束的時候翻轉這個cur white的位,從而使得這之前創建的白色obj都是old的,在sweep階段能夠得到正確釋放。
2.引用屏障。
在mark階段,黑色節點只能指向黑色或灰色節點,不能指向白色節點;灰色節點可以指向灰色節點或白色節點。換句話說,灰色節點充當了黑、白節點間的屏障作用(圖中的紅色虛線)。這一屏障功能是合理的,想象一下,如果黑色節點跨越屏障引用到了白色節點,說明該白色節點實際上是被引用狀態,不應該被GC釋放。但是由於黑色節點已經遍歷過,不會再重新遍歷,會導致Collector在后面sweep階段看到該obj為白色而將其錯誤的釋放。
為了保證這一屏障功能,lua內部實現了barrier機制。
-
-
-
-
參考資料
-
[Lua程序設計(第4版)]("https://item.jd.com/12384305.html ")