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万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章