lua資料結構之table的內部實現
一、table結構
1、Table結構體
首先了解一下table結構的組成結構,table是存放在GCObject裡的。結構如下:
從table的結構可以看出,table在設計的時候以兩種結構來存放資料。一般情況對於整數key,會用array來存放,而其它資料型別key會存放在雜湊表上。並且用lsizenode作為連結串列的長度,sizearray作為陣列長度。typedef struct Table { CommonHeader; lu_byte flags; /* 1<<p means tagmethod(p) is not present */ lu_byte lsizenode; /* 以2的lsizenode次方作為雜湊表長度 */ struct Table *metatable /* 元表 */; TValue *array; /* 陣列 */ Node *node; /* 雜湊表 */ Node *lastfree; /* 指向最後一個為閒置的連結串列空間 */ GCObject *gclist; int sizearray; /* 陣列的大小 */ } Table;
2、Node結構體
typedef union TKey { struct { TValuefields; struct Node *next; /* 指向下一個衝突node */ } nk; TValue tvk; } TKey; typedef struct Node { TValue i_val; TKey i_key; } Node;
Node結構很好理解,就是一個鍵值對的結構。主要是TKey結構,這裡用了union,所以TKey的大小是nk的大小。並且實際上TValue與TValuefields是同一個結構,因此tvk與nk的TValuefields都是代表鍵值。而且這裡有一個連結串列結構struct Node *next
,用於指向下一個有衝突的node。
二、建立table
table的建立通過lua_newtable
函式實現。通過定位具體實現是在luaH_new這個函式進行table的建立。程式碼如下:
Table *luaH_new (lua_State *L, int narray, int nhash) { Table *t = luaM_new(L, Table);/* new一個table物件 */ luaC_link(L, obj2gco(t), LUA_TTABLE); t->metatable = NULL; t->flags = cast_byte(~0); /* temporary values (kept only if some malloc fails) */ t->array = NULL; t->sizearray = 0; t->lsizenode = 0; t->node = cast(Node *, dummynode); setarrayvector(L, t, narray); setnodevector(L, t, nhash); return t; }
主要是對table進行初始化,其中setarrayvector是對陣列大小進行設定,setnodevector是對hash表大小進行設定,具體程式碼如下:
/*
設定陣列的容量
*/
static void setarrayvector (lua_State *L, Table *t, int size) {
int i;
//重新設定陣列的大小
luaM_reallocvector(L, t->array, t->sizearray, size, TValue);
//迴圈把陣列元素初始化為nil型別
for (i=t->sizearray; i<size; i++)
setnilvalue(&t->array[i]);
t->sizearray = size;
}
/*
設定雜湊表的容量
*/
static void setnodevector (lua_State *L, Table *t, int size) {
int lsize;
if (size == 0) { /* no elements to hash part? */
t->node = cast(Node *, dummynode); /* use common `dummynode' */
lsize = 0;
}
else {
int i;
//實際大小轉化為指數形式
lsize = ceillog2(size);
if (lsize > MAXBITS)
luaG_runerror(L, "table overflow");
//這裡實際大小以2的lsize次方來算的
size = twoto(lsize);
//建立指定大小的空間
t->node = luaM_newvector(L, size, Node);
//迴圈初始化每個node
for (i=0; i<size; i++) {
Node *n = gnode(t, i);
gnext(n) = NULL;
setnilvalue(gkey(n));
setnilvalue(gval(n));
}
}
t->lsizenode = cast_byte(lsize);
t->lastfree = gnode(t, size); /* 由於是新建立的,所以指向最後一個node,即指向下標為size的node */
}
三、插入鍵值鍵值的插入流程:
如圖所示,已經大致可以清楚新鍵產生的過程。接下來會分析比較重要的幾個模組。
1、table空間的動態擴充套件
無論是array還是hash表,都是以2的倍數進行擴充套件的。比較有區別的是,array陣列sizearray記錄的是真實大小,而hash表的lsizenode記錄的是2的倍數。當hash表空間滿的時候,才會重新分配array和hash表。比較重要的兩個函式是rehash和resize,前一個是重新算出要分配的空間,後一個是建立空間。先分析下rehash函式:
//加入key,重新分配hash與array的空間
static void rehash (lua_State *L, Table *t, const TValue *ek) {
int nasize, na;//nasize前期累計整數key個數,後期做為陣列空間大小,na表示陣列不為nil的個數
int nums[MAXBITS+1]; /* 累計各個區間整數key不為nil的個數,包括hash, 例如nums[i]表示累計在[2^(i-1),2^i]區間內的整數key個數*/
int i;
int totaluse;//記錄所有已存在的鍵,包括hash和array,即table裡的成員個數
for (i=0; i<=MAXBITS; i++) nums[i] = 0; /* 初始化所有計數區間*/
nasize = numusearray(t, nums); /* 以區間統計數組裡不為nil的個數,並獲得總數*/
totaluse = nasize; /* all those keys are integer keys */
totaluse += numusehash(t, nums, &nasize); /* 統計hash表裡已有的鍵,以及整數鍵的個數已經區間分佈*/
//如果新key是整數型別的情況
nasize += countint(ek, nums);
//累計新key
totaluse++;
/* 重新計算陣列空間 */
na = computesizes(nums, &nasize);
/* 重新建立記憶體空間, nasize為新陣列大小,totaluse - na表示所有鍵的個數減去新陣列的個數,即為新hash表需要存放的個數 */
resize(L, t, nasize, totaluse - na);
}
/*
重新分配陣列和hash表空間
*/
static void resize (lua_State *L, Table *t, int nasize, int nhsize) {
int i;
int oldasize = t->sizearray;
int oldhsize = t->lsizenode;
Node *nold = t->node; /* 儲存當前的hash表,用於後面建立新hash表時,可以重新對各個node賦值*/
if (nasize > oldasize) /* 是否需要擴充套件陣列 */
setarrayvector(L, t, nasize);
/* 重新分配hash空間*/
setnodevector(L, t, nhsize);
if (nasize < oldasize) { /* 小於之前大小,即有部分整數key放到了hash裡 */
t->sizearray = nasize;
/* 超出部分存放到hash表裡*/
for (i=nasize; i<oldasize; i++) {
if (!ttisnil(&t->array[i]))
setobjt2t(L, luaH_setnum(L, t, i+1), &t->array[i]);
}
/* 重新分配陣列空間,去掉後面溢位部分*/
luaM_reallocvector(L, t->array, oldasize, nasize, TValue);
}
/* 從後到前遍歷,把老hash表的值搬到新表中*/
for (i = twoto(oldhsize) - 1; i >= 0; i--) {
Node *old = nold+i;
if (!ttisnil(gval(old)))
setobjt2t(L, luaH_set(L, t, key2tval(old)), gval(old));
}
//釋放老hash表空間
if (nold != dummynode)
luaM_freearray(L, nold, twoto(oldhsize), Node); /* free old array */
}
2、鍵的建立規則
2、1整數型別
一般情況下,整數型別的鍵都是放在數組裡的,但是有2種特殊情況會被分配到hash表裡。
對於存放在陣列有一個規則,每插入一個整數key時,都要判斷包含當前key的區間[1, 2^n]裡,是否滿足table裡所有整數型別key的數量大於2^(n - 1),如果不成立則需要把這個key放在hash表裡。這樣設計,可以減少空間上的浪費,並可以進行空間的動態擴充套件。例如:
a[0] = 1, a[1] = 1, a[5]= 1
結果分析:陣列大小4, hash大小1,a[5]本來是在8這個區間裡的,但是有用個數3 < 8 / 2,所以a[5]放在了hash表裡。
a[0] = 1, a[1] = 1, a[5] = 1, a[6] = 1,
結果分析:陣列大小4,hash大小2,有用個數4 < 8 / 2,所以a[5],a[6]放在hash表裡。
a[0] = 1, a[1] = 1, a[5] = 1, a[6] = 1, a[7] = 1
結果分析:陣列大小8,hash大小0, 有用個數5 > 8 / 2。
陣列大小的規定由以下函式實現:
static int computesizes (int nums[], int *narray) {
int i;
int twotoi; /* 2^i */
int a = 0; /* 統計到2^i位置不為空的數量 */
int na = 0; /* 記錄重新調整後的不為空的數量 */
int n = 0; /* 記錄重新調整後的陣列大小 */
for (i = 0, twotoi = 1; twotoi/2 < *narray; i++, twotoi *= 2) {
if (nums[i] > 0) {
a += nums[i];
if (a > twotoi/2) { /* 判斷當前的數量是否滿足大於2^(i - 1) */
n = twotoi; /* optimal size (till now) */
na = a; /* all elements smaller than n will go to array part */
}
}
if (a == *narray) break; /* all elements already counted */
}
*narray = n;
lua_assert(*narray/2 <= na && na <= *narray);
return na;
}
通過前面的分析,可以清楚的知道這個函式的意圖,根據統計出來的所有整數鍵重新劃分資料大小。其中引數nums[]是一個數組,每個nums[i]都記錄了在陣列中[2^i, 2^(i + 1)]的區間內不為空的數量。引數narray是個指標,獲得重新調整後的陣列大小。
另外還有一種被分配到hash表裡的情況,當hash表有空間並且當前key值越界的時候,會先放在hash表裡,直到hash表滿的時候,才會把hash表裡的所有整數鍵按上面的方法進行操作。
2、2其他型別
對於非整數型別的鍵會被全部分配到hash表裡。前面我們提到,table用一個Node陣列來作為hash表的容器,利用hash演算法把鍵轉化為某個陣列下標,來進行存放。hash表在設計的時候有一點比較巧妙的地方,我們知道hash算出來的位置有可能是會衝突的,所以如果當前插入的key發生衝突的時候,會把當前插入的key放到lastfree中,並把當前的node連結到衝突鏈中。這裡有一種情況,如果插入的newkey的位置不是因為衝突而被佔,而是其他oldkey因為衝突暫時存放的話,會把這個位置讓會給原本屬於這個位置的newkey,並把oldkey放到lastfree中。可能不好理解,還是來模擬解釋下:
位置被佔的情況:
接下來再來看下主要程式碼就比較容易理解了:
static TValue *newkey (lua_State *L, Table *t, const TValue *key) {
Node *mp = mainposition(t, key);
//兩種情況,主鍵有值的情況很好理解。另一種,是t->node沒有分配空間的情況,即第一次插入的情況。
if (!ttisnil(gval(mp)) || mp == dummynode) {
Node *othern;
Node *n = getfreepos(t); /* 獲得lastfree指向的空間 */
if (n == NULL) { /* 沒有空間 */
rehash(L, t, key); /* 重整hash和array的大小 */
return luaH_set(L, t, key); /* re-insert key into grown table */
}
lua_assert(n != dummynode);
othern = mainposition(t, key2tval(mp));
//這裡想了很久終於明白為什麼有othern != mp這種情況,表示mp這個node原本不屬於這個位置的,只是佔用而已。
//因為mainposition取出來的node,有可能本來就不是存放在這個位置的。而是之前與某一個位置衝突,而放在lastfree裡的。
//所以othern != mp這種情況,表示的是原來不應存放在這個位置的node移到lastfree,而這個位置被新node佔據。
if (othern != mp) { /* is colliding node out of its main position? */
/* yes; move colliding node into free position */
//這裡是遍歷找到mp的前一個衝突節點
while (gnext(othern) != mp) othern = gnext(othern); /* find previous */
//把othern的下一個節點(即mp的位置)指向lastfree,mp的值賦值給lastfree
gnext(othern) = n; /* redo the chain with `n' in place of `mp' */
*n = *mp; /* copy colliding node into free pos. (mp->next also goes) */
gnext(mp) = NULL; /* now `mp' is free */
setnilvalue(gval(mp));
}
//表示mp的位置原本就是屬於這個位置的。也就是說與這個位置的雜湊值是碰撞的。
else { /* colliding node is in its own main position */
/* new node will go into free position */
//這裡新node(即n)是連結在衝突鏈的第二個位置
gnext(n) = gnext(mp); /* chain new position */
gnext(mp) = n;
mp = n;
}
}
gkey(mp)->value = key->value; gkey(mp)->tt = key->tt;
luaC_barriert(L, t, key);
lua_assert(ttisnil(gval(mp)));
return gval(mp);
}
3、鍵值的賦值
鍵值對賦值的過程,就是通過獲取棧頂的前兩個位置作為key和value,如果這個key在table裡是不存在的則建立新的key,並返回key對應的TValue指標,再對指標其進行賦值。具體實現如下:
/*
對table插入key與值
*/
void luaV_settable (lua_State *L, const TValue *t, TValue *key, StkId val) {
int loop;
TValue temp;
//這裡迴圈100次,是因為要遍歷所有的元表有無對應的key
for (loop = 0; loop < MAXTAGLOOP; loop++) {
const TValue *tm;
if (ttistable(t)) { /* `t' is a table? */
Table *h = hvalue(t);
//判斷這個key是否存在,如果沒有建立一個
TValue *oldval = luaH_set(L, h, key); /* do a primitive set */
//如果是已有的node會通過第一個條件,如果是新的node,判斷是否有元表
//也就是說,不會執行裡面的判斷只有一種可能,就是有_newindex這個元表
if (!ttisnil(oldval) || /* result is no nil? */
(tm = fasttm(L, h->metatable, TM_NEWINDEX)) == NULL) { /* or no TM? */
//把val賦值給oldval
setobj2t(L, oldval, val);
h->flags = 0;
luaC_barriert(L, h, val);
return;
}
/* else will try the tag method */
}
//如果元表為nil,報錯。
else if (ttisnil(tm = luaT_gettmbyobj(L, t, TM_NEWINDEX)))
luaG_typeerror(L, t, "index");
//如果是function,則執行這個function
if (ttisfunction(tm)) {
callTM(L, tm, t, key, val);
return;
}
/* else repeat with `tm' */
setobj(L, &temp, tm); /* avoid pointing inside table (may rehash) */
t = &temp;
}
luaG_runerror(L, "loop in settable");
}
四、for迴圈的分析
1、for迴圈的入棧操作
在lua裡,table.foreach(key, value)可以遍歷整一個table,因此來對foreach具體分析一下。foreach函式的作用,是從table裡迴圈查詢下一個key和value,壓入棧頂,並與lua層定義的function一起進行處理的一個過程。具體程式碼如下:
static int foreach (lua_State *L) {
luaL_checktype(L, 1, LUA_TTABLE);
luaL_checktype(L, 2, LUA_TFUNCTION);
lua_pushnil(L); /* 這裡把nil作為初始key,會用作儲存第一個key*/
//每遍歷一遍,一定是當前key在棧頂。
while (lua_next(L, 1)) {
lua_pushvalue(L, 2); /* function */
lua_pushvalue(L, -3); /* key */
lua_pushvalue(L, -3); /* value */
lua_call(L, 2, 1); /* 執行funciton */
if (!lua_isnil(L, -1))
return 1;
lua_pop(L, 2); /* 這裡彈出函式呼叫的返回值,和value,即棧頂為當前key */
}
return 0;
}
棧的活動模型流程如圖:
->->->->->->
2、鍵值的查詢
鍵值的查詢必定是先從陣列開始找,陣列找完了之後才按hash表找。並且都是以下標從小到大的順序遍歷,每次找到之後,都會把key存放起來,並把對應的value壓棧。下一次迴圈時在算出當前key的位置下標,通過位置下標往後移一單位來獲得下一個key。流程如圖:
具體的程式碼如下:
int luaH_next (lua_State *L, Table *t, StkId key) {
//返回key對應的下標,如果是在hash表裡的,是返回下標加上sizearray;
如果是第一次查詢則返回-1
int i = findindex(L, t, key);
//i++,即會從下一個key值開始遍歷,因為有可能是空的,所以需要遍歷到不為空為止。
for (i++; i < t->sizearray; i++) { /* try first array part */
if (!ttisnil(&t->array[i])) { /* a non-nil value? */
setnvalue(key, cast_num(i+1));
//在棧裡面,value賦值給棧頂的空位置,即L->top = &t->array[i]
//在函式外面會執行L->top++的。
setobj2s(L, key+1, &t->array[i]);
return 1;
}
}
//i - t->sizearray,求出hash下標的真正位置
for (i -= t->sizearray; i < sizenode(t); i++) { /* then hash part */
if (!ttisnil(gval(gnode(t, i)))) { /* a non-nil value? */
setobj2s(L, key, key2tval(gnode(t, i)));
setobj2s(L, key+1, gval(gnode(t, i)));
return 1;
}
}
return 0; /* no more elements */
}