Lua 5.3 源碼分析(六) 字符串 Table

Lua 5.3 源碼分析 (六) 表 Table

typedef union TKey {
  struct {
    TValuefields;
    int next;  /* for chaining (offset for next node) */
  } nk;
  TValue tvk;
} TKey;


/* copy a value into a key without messing up field 'next' */
#define setnodekey(L,key,obj) \
    { TKey *k_=(key); const TValue *io_=(obj); \
      k_->nk.value_ = io_->value_; k_->nk.tt_ = io_->tt_; \
      (void)L; checkliveness(G(L),io_); }


typedef struct Node {
  TValue i_val;
  TKey i_key;
} Node;


typedef struct Table {
  CommonHeader;
  lu_byte flags;  /* 1<<p means tagmethod(p) is not present */
  lu_byte lsizenode;  /* log2 of size of 'node' array */
  unsigned int sizearray;  /* size of 'array' array */
  TValue *array;  /* array part */
  Node *node;
  Node *lastfree;  /* any free position is before this position */
  struct Table *metatable;
  GCObject *gclist;
} Table;

table 的存儲分爲 數組部分和哈希表部分。
數組部分索引從1開始。
nil 是唯一不能做哈希鍵值的類型。
\# 對table 取長度時,也被定義爲 整數下表有關,而不是整個table 的長度。

Table *luaH_new (lua_State *L) {
  GCObject *o = luaC_newobj(L, LUA_TTABLE, sizeof(Table));
  Table *t = gco2t(o);
  t->metatable = NULL;
  t->flags = cast_byte(~0);
  t->array = NULL;
  t->sizearray = 0;
  setnodevector(L, t, 0);
  return t;
}


void luaH_free (lua_State *L, Table *t) {
  if (!isdummy(t->node))
    luaM_freearray(L, t->node, cast(size_t, sizenode(t)));
  luaM_freearray(L, t->array, t->sizearray);
  luaM_free(L, t);
}

用isdummy 判斷 哈希表部分是否爲空表,當哈希表部分爲空時,在析構函數中只需要釋放 數組部分佔用內存。

數組部分

Table 的數組部分被存儲在TValue *array 中, int sizearray 存儲着數組長度。

哈希表部分

Table 的哈希表部分被存儲在 Node *node , lu_byte lsizenode 存儲着哈希表的大小。由於哈希表的大小一定是 2 的整數次冪,所以這裏的 lsizenode 表示的是 次冪數,而不是實際大小 (2^lsizenode)。

定義了一個不可寫的空哈希表:dummynode , setnodevector 函數用來初始化哈希表部分 , 當 size 參數爲0 時,說明是一個空表被初始化,則node 域指向這個dummynode 節點。

#define dummynode       (&dummynode_)

#define isdummy(n)      ((n) == dummynode)

static const Node dummynode_ = {
  {NILCONSTANT},  /* value */
  {{NILCONSTANT, 0}}  /* key */
};

表的增刪改查

刪除

表沒有刪除操作,使用將對應的鍵位賦值爲nil。

讀取

使用 luaH_newkey 負責在哈希表中創建一個不存在的鍵位,不影響數組部分。

TValue *luaH_newkey (lua_State *L, Table *t, const TValue *key) {
  Node *mp;
  TValue aux;
  if (ttisnil(key))
      luaG_runerror(L, "table index is nil");
  else if (ttisfloat(key)) {
    lua_Number n = fltvalue(key);
    lua_Integer k;
    if (luai_numisnan(n))
      luaG_runerror(L, "table index is NaN");
    if (numisinteger(n, &k)) {  /* index is int? */
      setivalue(&aux, k);
      key = &aux;  /* insert it as an integer */
    }
  }
  mp = mainposition(t, key);
  if (!ttisnil(gval(mp)) || isdummy(mp)) {  /* main position is taken? */
    Node *othern;
    Node *f = getfreepos(t);  /* get a free place */
    if (f == NULL) {  /* cannot find a free place? */
      rehash(L, t, key);  /* grow table */
      /* whatever called 'newkey' takes care of TM cache and GC barrier */
      return luaH_set(L, t, key);  /* insert key into grown table */
    }
    lua_assert(!isdummy(f));
    othern = mainposition(t, gkey(mp));
    if (othern != mp) {  /* is colliding node out of its main position? */
      /* yes; move colliding node into free position */
      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 {  /* colliding node is in its own main position */
      /* new node will go into free position */
      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);
}

哈希表以閉散列(開放地址法)方式實現。每個可能的鍵值在哈希表中都有一個 mainposition。創建一個新鍵位時,需要檢查mainposition ,若沒有則創建新鍵;若之前已有其它鍵位佔據了這個位置,則檢查佔據此位置的鍵位的mainposition 是不是這裏。

static Node *mainposition (const Table *t, const TValue *key) {
  switch (ttype(key)) {
    case LUA_TNUMINT:
      return hashint(t, ivalue(key));
    case LUA_TNUMFLT:
      return hashfloat(t, fltvalue(key));
    case LUA_TSHRSTR:
      return hashstr(t, tsvalue(key));
    case LUA_TLNGSTR: {
      TString *s = tsvalue(key);
      if (s->extra == 0) {  /* no hash? */
        s->hash = luaS_hash(getstr(s), s->len, s->hash);
        s->extra = 1;  /* now it has its hash */
      }
      return hashstr(t, tsvalue(key));
    }
    case LUA_TBOOLEAN:
      return hashboolean(t, bvalue(key));
    case LUA_TLIGHTUSERDATA:
      return hashpointer(t, pvalue(key));
    case LUA_TLCF:
      return hashpointer(t, fvalue(key));
    default:
      return hashpointer(t, gcvalue(key));
  }
}

如果兩者位置衝突,則利用 Node 結構中的 next 域 以一個單向鏈表的形式把它們鏈接起來;反之,新鍵佔據這個位置,而舊鍵更換到新位置並根據它的主鍵找到屬於它的鏈的那條單向鏈表中的一個節點,重新鏈入。

無論哪種衝突情況,都需要在哈希表中找到一個空閒可用的節點。getfreepos 函數遞減 lastfree 域來實現這個功能。

static Node *getfreepos (Table *t) {
  while (t->lastfree > t->node) {
    t->lastfree--;
    if (ttisnil(gkey(t->lastfree)))
      return t->lastfree;
  }
  return NULL;  /* could not find a free place */
}

Lua 不會再設置鍵位的值爲nil 時回收內存,而是預先準備好的哈希空間使用完後惰性回收(即在lastfree 遞減到哈希空間的頭時,做一次rehash 操作)。

/*
** nums[i] = number of keys 'k' where 2^(i - 1) < k <= 2^i
*/
static void rehash (lua_State *L, Table *t, const TValue *ek) {
  unsigned int nasize, na;
  unsigned int nums[MAXABITS + 1];
  int i;
  int totaluse;
  for (i = 0; i <= MAXABITS; i++) nums[i] = 0;  /* reset counts */
  nasize = numusearray(t, nums);  /* count keys in array part */
  totaluse = nasize;  /* all those keys are integer keys */
  totaluse += numusehash(t, nums, &nasize);  /* count keys in hash part */
  /* count extra key */
  nasize += countint(ek, nums);
  totaluse++;
  /* compute new size for array part */
  na = computesizes(nums, &nasize);
  /* resize the table to new computed sizes */
  luaH_resize(L, t, nasize, totaluse - na);
}

rehash 統計當前table 中到底有多少有效鍵值對,以及決定數組部分需要開闢多少內存空間。
lua 使用一個rehash函數中定義在棧上的nums 數組來做這個整數鍵統計工作。這個數組按 2的整數次冪分開統計各個區間段的整數鍵個數。
統計過程分別用 numusearray 與 numusehash 來實現 數組、哈希表部分。

computesizes 函數計算出不低於50% 利用率下,數組該維持多少空間。同時還可以得到有效鍵將被存儲在哈希表中。

根據這些統計數據,rehash函數調用 luaH_resize 來重新調整 數組部分和哈希部分的大小,把不能放在數組裏的鍵值對 重新裝入哈希表。

查詢

luaH_get 函數實現查詢操作;
1. 當查詢的Key 爲整數且在數組範圍內時,調用 luaH_getint 函數 在表的數組部分查詢。
2. 否則,根據 Key 的哈希值去哈希表部分查詢。
3. 當擁有相同哈希值的衝突鍵值對時,在哈希表中由於Node 的next 域單向鏈接起來,所以遍歷這個鏈表即可。
4. 當Key 爲短字符串時,調用 luaH_getstr 函數(避免逐個字節比較字符串)

     /*
    ** search function for integers
    */
    const TValue *luaH_getint (Table *t, lua_Integer key) {
      /* (1 <= key && key <= t->sizearray) */
      if (l_castS2U(key - 1) < t->sizearray)
        return &t->array[key - 1];
      else {
        Node *n = hashint(t, key);
        for (;;) {  /* check whether 'key' is somewhere in the chain */
          if (ttisinteger(gkey(n)) && ivalue(gkey(n)) == key)
            return gval(n);  /* that's it */
          else {
            int nx = gnext(n);
            if (nx == 0) break;
            n += nx;
          }
        };
        return luaO_nilobject;
      }
    }


    /*
    ** search function for short strings
    */
    const TValue *luaH_getstr (Table *t, TString *key) {
      Node *n = hashstr(t, key);
      lua_assert(key->tt == LUA_TSHRSTR);
      for (;;) {  /* check whether 'key' is somewhere in the chain */
        const TValue *k = gkey(n);
        if (ttisshrstring(k) && eqshrstr(tsvalue(k), key))
          return gval(n);  /* that's it */
        else {
          int nx = gnext(n);
          if (nx == 0) break;
          n += nx;
        }
      };
      return luaO_nilobject;
    }

短字符串

以短字符串作爲鍵的情況非常常見,Lua 對此做了一些優化。
對於小於LUAI_MAXSHORTLEN,默認爲40 的短字符串做內部唯一化處理。相同的短字符串在同一個 Lua_State 中只會存一份。這可以簡化字符串的比較操作。

長字符串不做內部唯一化操作,並且其哈希值也是惰性計算的。

數字類型的哈希值

以數字類型爲鍵且沒有置入數組部分時,需要對它們取哈希值,便於放進哈希表中。

float

/*
** hash for floating-point numbers
*/
static Node *hashfloat (const Table *t, lua_Number n) {
  int i;
  n = l_mathop(frexp)(n, &i) * cast_num(INT_MAX - DBL_MAX_EXP);
  i += cast_int(n);
  if (i < 0) {
    if (cast(unsigned int, i) == 0u - i)  /* use unsigned to avoid overflows */
      i = 0;  /* handle INT_MIN */
    i = -i;  /* must be a positive value */
  }
  return hashmod(t, i);
}

int

迭代器

給出一個next方法。出入一個 Key,返回下一個鍵值對。

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

它嘗試返回傳入的 Key 在數組部分的下一個非空值。當超過數組部分後,則檢索哈希表中的相對應位置,並返回哈希表中對應節點在存儲空間分佈上的下一個節點處的鍵值對。

遍歷一個table 的過程中,向這個table 中插入一個新鍵這種行爲,將會無法預測後續的遍歷行爲,但是Lua 卻允許在遍歷過程中,修改 table 中已經存在的鍵對應的值。

lua 沒有顯式的從table 中刪除鍵的操作,只能對不需要的鍵設爲空。

一旦在迭代過程中發生了GC ,對鍵值賦值爲nil 的操作就有可能導致 GC 過程中把這個鍵值對 標記爲死鍵。 所以在next 操作中,從上一個鍵定位下一個鍵的過程中,需要支持檢索一個死鍵,查詢這個死鍵的下一個鍵位。findindex 函數提供這個功能:

static unsigned int findindex (lua_State *L, Table *t, StkId key) {
  unsigned int i;
  if (ttisnil(key)) return 0;  /* first iteration */
  i = arrayindex(key);
  if (i != 0 && i <= t->sizearray)  /* is 'key' inside array part? */
    return i;  /* yes; that's the index */
  else {
    int nx;
    Node *n = mainposition(t, key);
    for (;;) {  /* check whether 'key' is somewhere in the chain */
      /* key may be dead already, but it is ok to use it in 'next' */
      if (luaV_rawequalobj(gkey(n), key) ||
            (ttisdeadkey(gkey(n)) && iscollectable(key) &&
             deadvalue(gkey(n)) == gcvalue(key))) {
        i = cast_int(n - gnode(t, 0));  /* key index in hash table */
        /* hash elements are numbered after array ones */
        return (i + 1) + t->sizearray;
      }
      nx = gnext(n);
      if (nx == 0)
        luaG_runerror(L, "invalid key to 'next'");  /* key not found */
      else n += nx;
    }
  }
}

lua 的 table 的長度定義只對序列表有效。所以,在實現的時候,僅需要遍歷table 的數組部分。 只有當數組部分填滿時才需要進一步的去檢索哈希表。使用二分法來快速在哈希表中定位一個非nil 的整數鍵的位置。

int luaH_getn (Table *t) {
  unsigned int j = t->sizearray;
  if (j > 0 && ttisnil(&t->array[j - 1])) {
    /* there is a boundary in the array part: (binary) search for it */
    unsigned int i = 0;
    while (j - i > 1) {
      unsigned int m = (i+j)/2;
      if (ttisnil(&t->array[m - 1])) j = m;
      else i = m;
    }
    return i;
  }
  /* else must find a boundary in hash part */
  else if (isdummy(t->node))  /* hash part is empty? */
    return j;  /* that is easy... */
  else return unbound_search(t, j);
}


static int unbound_search (Table *t, unsigned int j) {
  unsigned int i = j;  /* i is zero or a present index */
  j++;
  /* find 'i' and 'j' such that i is present and j is not */
  while (!ttisnil(luaH_getint(t, j))) {
    i = j;
    if (j > cast(unsigned int, MAX_INT)/2) {  /* overflow? */
      /* table was built with bad purposes: resort to linear search */
      i = 1;
      while (!ttisnil(luaH_getint(t, i))) i++;
      return i - 1;
    }
    j *= 2;
  }
  /* now do a binary search between them */
  while (j - i > 1) {
    unsigned int m = (i+j)/2;
    if (ttisnil(luaH_getint(t, m))) j = m;
    else i = m;
  }
  return i;
}

元表

發佈了198 篇原創文章 · 獲贊 36 · 訪問量 27萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章