1. 程式人生 > >lua資料結構之table的內部實現

lua資料結構之table的內部實現

一、table結構

1、Table結構體

首先了解一下table結構的組成結構,table是存放在GCObject裡的。結構如下:

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;
從table的結構可以看出,table在設計的時候以兩種結構來存放資料。一般情況對於整數key,會用array來存放,而其它資料型別key會存放在雜湊表上。並且用lsizenode作為連結串列的長度,sizearray作為陣列長度。

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 */
}