Lua篇 — 表

Posted by Xun on Monday, August 7, 2023

lua 表是 lua 中的非常常用的结构,本文将以 lua-5.4.1 版本进行源码解析。

简介

  • 在 C# 中,有数组、字典等数据结构,用于存储一批数据并提供快速查询的方法。而在 lua 中,则通过表(table),来实现类似的数据结构。

数据结构

  • 在 lua 中,table 的数据结构为:
// lobject.h

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 alimit;  /* "limit" 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 的参数含义为:
    • CommonHeader :需要 gc 的对象的公共头部。
    • flags :表中拥有的方法标识,方法类型定义在 TMS 枚举(ltm.h)中。
    • lsizenode :node 长度对 2 取对数。
    • alimit :array 的数量上限。
    • array :数组部分。
    • node :哈希表部分。
    • lastfree :上一个空闲节点。
    • metatable :元表。
    • gclist :gc 相关信息。
  • 可以看到,在 lua 中的表,是一个复合结构,同时具有数组和哈希表的功能,后面将介绍其具体实现。

创建 table

  • lua 创建新表的方法为:
// lapi.c

LUA_API void lua_createtable (lua_State *L, int narray, int nrec) {
  Table *t;
  lua_lock(L);
  t = luaH_new(L);
  sethvalue2s(L, L->top, t);
  api_incr_top(L);
  if (narray > 0 || nrec > 0)
    luaH_resize(L, t, narray, nrec);
  luaC_checkGC(L);
  lua_unlock(L);
}
  • 创建新表的主要流程为:
    • 调用 luaH_new 创建一个 Table 对象。
    • 将 Table 对象设置到栈上。
    • 移动栈顶到下一个位置。
    • 根据传入的数组长度和哈希表长度,重新设置表对象的大小。
  • luaH_new 的方法为:
// ltable.c

Table *luaH_new (lua_State *L) {
  GCObject *o = luaC_newobj(L, LUA_VTABLE, sizeof(Table));
  Table *t = gco2t(o);
  t->metatable = NULL;
  t->flags = cast_byte(maskflags);  /* table has no metamethod fields */
  t->array = NULL;
  t->alimit = 0;
  setnodevector(L, t, 0);
  return t;
}
  • 创建 Table 对象时,元表默认为空,flags 设置为 maskflags ,从右往左前 6 位为 1 ,其他位为 0 ,其中,第 8 位用于代表当前 alimit 是否为数组的真实上限。
  • table 的主要对象即为数组和哈希表部分。创建时,数组设置为 NULL ,数组上限大小也为 0 。

哈希表初始化

  • 哈希表通过 setnodevector 进行初始化:
// ltable.c

static void setnodevector (lua_State *L, Table *t, unsigned int size) {
  if (size == 0) {  /* no elements to hash part? */
    t->node = cast(Node *, dummynode);  /* use common 'dummynode' */
    t->lsizenode = 0;
    t->lastfree = NULL;  /* signal that it is using dummy node */
  }
  else {
    int i;
    int lsize = luaO_ceillog2(size);
    if (lsize > MAXHBITS || (1u << lsize) > MAXHSIZE)
      luaG_runerror(L, "table overflow");
    size = twoto(lsize);
    t->node = luaM_newvector(L, size, Node);
    for (i = 0; i < (int)size; i++) {
      Node *n = gnode(t, i);
      gnext(n) = 0;
      setnilkey(n);
      setempty(gval(n));
    }
    t->lsizenode = cast_byte(lsize);
    t->lastfree = gnode(t, size);  /* all positions are free */
  }
}
  • 空的哈希表,lsizenode 为 0 ,则最小尺寸为 1 ,为了减少空表的 node 节点,因此创建了一个不可修改的静态节点 dummynode ,将空表的 node 指向 dummynode 。
// ltable.c

#define dummynode		(&dummynode_)

static const Node dummynode_ = {
  {{NULL}, LUA_VEMPTY,  /* value's value and type */
   LUA_VNIL, 0, {NULL}}  /* key type, next, and key value */
};


static const TValue absentkey = {ABSTKEYCONSTANT};
  • 对于非空的哈希表,则根据传入的尺寸 size ,向上取对数值,得到对数尺寸 lsizenode ,并申请对应的 Node 数组内存,对数组进行初始化赋值。

存入 table

  • 向 table 中写入值,可以通过调用 lua_settable 方法进行,其代码如下:
// lapi.c

LUA_API void lua_settable (lua_State *L, int idx) {
  TValue *t;
  const TValue *slot;
  lua_lock(L);
  api_checknelems(L, 2);
  t = index2value(L, idx);
  if (luaV_fastget(L, t, s2v(L->top - 2), slot, luaH_get)) {
    luaV_finishfastset(L, t, slot, s2v(L->top - 1));
  }
  else
    luaV_finishset(L, t, s2v(L->top - 2), s2v(L->top - 1), slot);
  L->top -= 2;  /* pop index and value */
  lua_unlock(L);
}
// lvm.c

#define luaV_fastget(L,t,k,slot,f) \
  (!ttistable(t)  \
   ? (slot = NULL, 0)  /* not a table; 'slot' is NULL and result is 0 */  \
   : (slot = f(hvalue(t), k),  /* else, do raw access */  \
      !isempty(slot)))  /* result not empty? */
  • 先通过 luaV_fastget 方法,对于 table 类型的对象,调用传入的获取方法 luaH_get 进行查找,找到对应位置如果数据不为空,则当前 key 值已经存在,需要将新的值更新到对应的位置中。如果不存在,则调用 luaV_finishset 进行新增 key 值相关处理。

查找存入位置更新现有 key

  • 查找 key 值的方法如下:
// ltable.c

const TValue *luaH_get (Table *t, const TValue *key) {
  switch (ttypetag(key)) {
    case LUA_VSHRSTR: return luaH_getshortstr(t, tsvalue(key));
    case LUA_VNUMINT: return luaH_getint(t, ivalue(key));
    case LUA_VNIL: return &absentkey;
    case LUA_VNUMFLT: {
      lua_Integer k;
      if (luaV_flttointeger(fltvalue(key), &k, F2Ieq)) /* integral index? */
        return luaH_getint(t, k);  /* use specialized version */
      /* else... */
    }  /* FALLTHROUGH */
    default:
      return getgeneric(t, key);
  }
}
  • 可以看到,对于传入的 key 值,根据不同类型进行 hash 计算后进行查找。其中,key 值为 float 类型的,会检查是否能转成 int 类型,使用 int 类型进行查找。而对于非 int 类型的,都是直接根据 hash 计算结果,在哈希表中查找。而 int 类型的 key ,则会在数组部分和哈希表部分中查找。
// ltable.c

const TValue *luaH_getint (Table *t, lua_Integer key) {
  if (l_castS2U(key) - 1u < t->alimit)  /* 'key' in [1, t->alimit]? */
    return &t->array[key - 1];
  else if (!limitequalsasize(t) &&  /* key still may be in the array part? */
           (l_castS2U(key) == t->alimit + 1 ||
            l_castS2U(key) - 1u < luaH_realasize(t))) {
    t->alimit = cast_uint(key);  /* probably '#t' is here now */
    return &t->array[key - 1];
  }
  else {
    Node *n = hashint(t, key);
    for (;;) {  /* check whether 'key' is somewhere in the chain */
      if (keyisinteger(n) && keyival(n) == key)
        return gval(n);  /* that's it */
      else {
        int nx = gnext(n);
        if (nx == 0) break;
        n += nx;
      }
    }
    return &absentkey;
  }
}
  • 查找过程有几种情况:
    • 如果索引 key 值,在数组上限数量 alimit 内,则数据直接存入数组对应索引位置中。
    • 如果数组上限数量 alimit 不为真实上限,但索引 key 值仍然在数组部分的真实上限内,同样存入数组对应索引的位置中。
    • 如果索引超过数组上限,则需要对索引 key 做 hash 计算,得到哈希表 node 的链表节点,遍历链表节点,找到节点的 key 值和索引相同的,该节点即为要存入数据的位置。
    • 如果没有找到对应节点,则返回一个常量位置,表示该索引 key 缺少可以存入的位置。
  • 其中,索引 hash 计算的方法为:
// ltable.c

#define hashpow2(t,n)		(gnode(t, lmod((n), sizenode(t))))
...

#define hashint(t,i)		hashpow2(t, i)
  • hashint 计算,就是将索引 key 对哈希表长度取模。由于哈希表的长度 size 为 2 的指数次幂,因此 size - 1 的有效位为连续的 1 ,则取模的结果是连续的,每一个有效位的 0 和 1 的出现概率都是相同的,一定程度上能得到均匀分布的结果。
    • 如果 size = 4,则 size - 1 二进制有效位表示为 11,取模结果有 00 、01 、10 、11 ,每一个有效位的 0 和 1 的出现概率都为 1/2 ,11 的下一个结果即为 00,保持有效位上的连续性。
    • 如果 size = 5 ,则 size - 1 二进制有效位表示为 100,则取模结果有 000 、001 、010 、011 、100 ,第一位出现 1 的概率为 1/5 ,第二位为 2/5 ,第三位为 2/5 ,100 的下一个结果不是 101 ,而是直接跳到 000 ,即有效位上不连续。
  • 此外,对于一些长字符串,hash 计算的结果可能很大,而取模时,只需要对后几位有效位进行计算即可得到结果,因此也提高了取模的计算效率。
  • 因此,lsizenode 使用 2 的对数结果来保存长度,来保证哈希表长度是 2 的指数次幂。

分配存入位置添加新 key

  • 经过查找后,没有找到位置,则需要重新进行分配后写入。
// lvm.c

void luaV_finishset (lua_State *L, const TValue *t, TValue *key,
                     TValue *val, const TValue *slot) {
  int loop;  /* counter to avoid infinite loops */
  for (loop = 0; loop < MAXTAGLOOP; loop++) {
    const TValue *tm;  /* '__newindex' metamethod */
    if (slot != NULL) {  /* is 't' a table? */
      Table *h = hvalue(t);  /* save 't' table */
      lua_assert(isempty(slot));  /* slot must be empty */
      tm = fasttm(L, h->metatable, TM_NEWINDEX);  /* get metamethod */
      if (tm == NULL) {  /* no metamethod? */
        if (isabstkey(slot))  /* no previous entry? */
          slot = luaH_newkey(L, h, key);  /* create one */
        /* no metamethod and (now) there is an entry with given key */
        setobj2t(L, cast(TValue *, slot), val);  /* set its new value */
        invalidateTMcache(h);
        luaC_barrierback(L, obj2gco(h), val);
        return;
      }
      /* else will try the metamethod */
    }
    else {  /* not a table; check metamethod */
      tm = luaT_gettmbyobj(L, t, TM_NEWINDEX);
      if (unlikely(notm(tm)))
        luaG_typeerror(L, t, "index");
    }
    /* try the metamethod */
    if (ttisfunction(tm)) {
      luaT_callTM(L, tm, t, key, val);
      return;
    }
    t = tm;  /* else repeat assignment over 'tm' */
    if (luaV_fastget(L, t, key, slot, luaH_get)) {
      luaV_finishfastset(L, t, slot, val);
      return;  /* done */
    }
    /* else 'return luaV_finishset(L, t, key, val, slot)' (loop) */
  }
  luaG_runerror(L, "'__newindex' chain too long; possible loop");
}
  • 可以看到,为新的 key 值分配位置时,是否有元表,会进行不同处理。
    • 当前 table 没有元表,或者元表没有 __newindex ,如果有找到未赋值的空闲位置,进行赋值,如果没有找到空闲位置,则调用 luaH_newkey 方法为新的 key 值分配位置后赋值。
    • 当前 table 元表 __newindex 为方法,调用 __newindex 方法进行分配位置后赋值。
    • 当前 table 元表 __newindex 不为方法,则在 __newindex 中进行查找位置。
      • 如果找到位置,则进行赋值。
      • 如果找不到位置,则将 __newindex 传入 luaV_finishset ,递归调用,直到分配到位置后进行赋值(调用上限为 2000 次)。
  • luaH_newkey 的代码如下:
// ltable.c

TValue *luaH_newkey (lua_State *L, Table *t, const TValue *key) {
  Node *mp;
  TValue aux;
  if (unlikely(ttisnil(key)))
    luaG_runerror(L, "table index is nil");
  else if (ttisfloat(key)) {
    lua_Number f = fltvalue(key);
    lua_Integer k;
    if (luaV_flttointeger(f, &k, F2Ieq)) {  /* does key fit in an integer? */
      setivalue(&aux, k);
      key = &aux;  /* insert it as an integer */
    }
    else if (unlikely(luai_numisnan(f)))
      luaG_runerror(L, "table index is NaN");
  }
  mp = mainpositionTV(t, key);
  if (!isempty(gval(mp)) || isdummy(t)) {  /* 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 */
      return luaH_set(L, t, key);  /* insert key into grown table */
    }
    lua_assert(!isdummy(t));
    othern = mainposition(t, keytt(mp), &keyval(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 */
      }
      setempty(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, key);
  luaC_barrierback(L, obj2gco(t), key);
  lua_assert(isempty(gval(mp)));
  return gval(mp);
}
  • 分配新位置的流程为:
    • 检查传入的 key 是否为 nil ,为 nil 则抛异常。
    • 如果 key 是 float 类型,则尝试转换成 int 类型。
    • 根据 key 的类型和值进行 hash 计算,根据计算结果查找存放的主节点位置。
    • 如果存放的主节点位置不为空(即已经存放了数据),或当前哈希表没有空闲位置,则需要进一步分配位置。
      • 如果当前哈希表已经没有空闲位置,就调用 rehash 方法,对哈希表进行扩容,再为 key 分配位置返回。
      • 如果当前哈希表还有空闲位置,但是当前主节点位置不为空,则根据该节点的 key 类型和值,计算其应该分配的位置,是否和主节点相同。
        • 如果主节点的节点信息计算出来的位置节点,和主节点相同,即两个节点的 key 具有相同的 hash 计算结果,则把空闲节点指向主节点的下一个节点,再把主节点指向空闲节点,形成的链表顺序为:主节点 -> 最后一次分配的节点 -> 倒数第二次分配的节点 -> ….
        • 如果主节点的节点信息计算出来的位置节点,和主节点不同,即两个节点的 key 的 hash 计算结果不一致,归属于两个链表,因此需要将主节点的数据转到空闲节点中,再链接到其对应的链表,将主节点作为新的 key 的存放节点。
    • 将 key 设置到分配的节点中,并返回节点位置。

读取 table

  • 读取 table 中的值,通过 lua_gettable 方法实现。
// lapi.c

LUA_API int lua_gettable (lua_State *L, int idx) {
  const TValue *slot;
  TValue *t;
  lua_lock(L);
  t = index2value(L, idx);
  if (luaV_fastget(L, t, s2v(L->top - 1), slot, luaH_get)) {
    setobj2s(L, L->top - 1, slot);
  }
  else
    luaV_finishget(L, t, s2v(L->top - 1), L->top - 1, slot);
  lua_unlock(L);
  return ttype(s2v(L->top - 1));
}
  • 和 lua_settable 一样,先获取当前 table 中是否有对应的 key 值。如果找到对应 key ,则调用 luaV_finishget 继续查找。
// lvm.c

void luaV_finishget (lua_State *L, const TValue *t, TValue *key, StkId val,
                      const TValue *slot) {
  int loop;  /* counter to avoid infinite loops */
  const TValue *tm;  /* metamethod */
  for (loop = 0; loop < MAXTAGLOOP; loop++) {
    if (slot == NULL) {  /* 't' is not a table? */
      lua_assert(!ttistable(t));
      tm = luaT_gettmbyobj(L, t, TM_INDEX);
      if (unlikely(notm(tm)))
        luaG_typeerror(L, t, "index");  /* no metamethod */
      /* else will try the metamethod */
    }
    else {  /* 't' is a table */
      lua_assert(isempty(slot));
      tm = fasttm(L, hvalue(t)->metatable, TM_INDEX);  /* table's metamethod */
      if (tm == NULL) {  /* no metamethod? */
        setnilvalue(s2v(val));  /* result is nil */
        return;
      }
      /* else will try the metamethod */
    }
    if (ttisfunction(tm)) {  /* is metamethod a function? */
      luaT_callTMres(L, tm, t, key, val);  /* call it */
      return;
    }
    t = tm;  /* else try to access 'tm[key]' */
    if (luaV_fastget(L, t, key, slot, luaH_get)) {  /* fast track? */
      setobj2s(L, val, slot);  /* done */
      return;
    }
    /* else repeat (tail call 'luaV_finishget') */
  }
  luaG_runerror(L, "'__index' chain too long; possible loop");
}
  • 和 luaV_finishset 流程相似,luaV_finishget 也会根据是否有元表,会进行不同处理。
    • 当前 table 没有元表,或者元表没有 __index ,则当前 table 不存在对应 key ,返回 nil 。
    • 当前 table 元表 __index 为方法,调用 __index 方法查找后返回结果。
    • 当前 table 元表 __index 不为方法,则在 __index 中进行查找。
      • 如果找到 key ,则将对应结果返回。
      • 如果找不到 key ,则将 __index 传入 luaV_finishget ,递归调用,返回结果(调用上限为 2000 次)。

设置 table 尺寸

  • 调用 lua_createtable 创建 table 时,如果传入了数组长度和哈希表长度,就会调用 luaH_resize 方法进行尺寸设置。
// ltable.c

void luaH_resize (lua_State *L, Table *t, unsigned int newasize,
                                          unsigned int nhsize) {
  unsigned int i;
  Table newt;  /* to keep the new hash part */
  unsigned int oldasize = setlimittosize(t);
  TValue *newarray;
  /* create new hash part with appropriate size into 'newt' */
  setnodevector(L, &newt, nhsize);
  if (newasize < oldasize) {  /* will array shrink? */
    t->alimit = newasize;  /* pretend array has new size... */
    exchangehashpart(t, &newt);  /* and new hash */
    /* re-insert into the new hash the elements from vanishing slice */
    for (i = newasize; i < oldasize; i++) {
      if (!isempty(&t->array[i]))
        luaH_setint(L, t, i + 1, &t->array[i]);
    }
    t->alimit = oldasize;  /* restore current size... */
    exchangehashpart(t, &newt);  /* and hash (in case of errors) */
  }
  /* allocate new array */
  newarray = luaM_reallocvector(L, t->array, oldasize, newasize, TValue);
  if (unlikely(newarray == NULL && newasize > 0)) {  /* allocation failed? */
    freehash(L, &newt);  /* release new hash part */
    luaM_error(L);  /* raise error (with array unchanged) */
  }
  /* allocation ok; initialize new part of the array */
  exchangehashpart(t, &newt);  /* 't' has the new hash ('newt' has the old) */
  t->array = newarray;  /* set new array part */
  t->alimit = newasize;
  for (i = oldasize; i < newasize; i++)  /* clear new slice of the array */
     setempty(&t->array[i]);
  /* re-insert elements from old hash part into new parts */
  reinsert(L, &newt, t);  /* 'newt' now has the old hash */
  freehash(L, &newt);  /* free old hash part */
}

  • table 设置尺寸的流程为:
    • 记录旧的数组长度。
    • 根据新的哈希表长度,创建一个临时哈希表。
    • 如果新的数组长度比旧的小,即要缩小数组大小,则:
      • 设置 table 的数组上限长度为新的长度。
      • 将 table 原本的哈希表数据和临时哈希表数据交换,即将 table 的哈希表数据设置为临时的哈希表数据。
      • 将数组部分超出上限的部分设置到临时哈希表中。
      • 恢复 table 的数组上限长度为旧的长度。
      • 将 table 的哈希表数据设置为原本的哈希表数据,数组部分额外的数据则保存在临时哈希表中。
    • 根据新的数组长度创建新的数组对象。
    • 将 table 的哈希表数据设置为临时哈希表数据,即保持哈希表优先设置数组的数据。
    • 更新 table 的数组和数组上限长度。
    • 将原本的哈希表数据重新插入到 table 中。
    • 释放临时哈希表。
  • 从前面读写的流程可以知道,table 的数组部分并不代表所有的数组数据,超出数组上限的部分会存放到哈希表中。因此,当哈希表没法再插入新的值时,需要调用 rehash 方法,计算需要扩容的尺寸。
// ltable.c

static void rehash (lua_State *L, Table *t, const TValue *ek) {
  unsigned int asize;  /* optimal size for array part */
  unsigned int na;  /* number of keys in the array part */
  unsigned int nums[MAXABITS + 1];
  int i;
  int totaluse;
  for (i = 0; i <= MAXABITS; i++) nums[i] = 0;  /* reset counts */
  setlimittosize(t);
  na = numusearray(t, nums);  /* count keys in array part */
  totaluse = na;  /* all those keys are integer keys */
  totaluse += numusehash(t, nums, &na);  /* count keys in hash part */
  /* count extra key */
  if (ttisinteger(ek))
    na += countint(ivalue(ek), nums);
  totaluse++;
  /* compute new size for array part */
  asize = computesizes(nums, &na);
  /* resize the table to new computed sizes */
  luaH_resize(L, t, asize, totaluse - na);
}
  • 为了确定扩容后需要的大小,对数组部分和哈希表部分进行统计。除了计算当前使用的所有位置数量,还要统计所有类型为 int 的 key,在 (2 ^ (i - 1), 2 ^ i] 区间的数量分布。key 类型为 int ,都认为 key 值为数组索引,优先选择存入数组部分,查找效率会比较高。统计完成后,通过调用 computesizes 方法,来确定最终的尺寸大小。
// ltable.c

static unsigned int computesizes (unsigned int nums[], unsigned int *pna) {
  int i;
  unsigned int twotoi;  /* 2^i (candidate for optimal size) */
  unsigned int a = 0;  /* number of elements smaller than 2^i */
  unsigned int na = 0;  /* number of elements to go to array part */
  unsigned int optimal = 0;  /* optimal size for array part */
  /* loop while keys can fill more than half of total size */
  for (i = 0, twotoi = 1;
       twotoi > 0 && *pna > twotoi / 2;
       i++, twotoi *= 2) {
    a += nums[i];
    if (a > twotoi/2) {  /* more than half elements present? */
      optimal = twotoi;  /* optimal size (till now) */
      na = a;  /* all elements up to 'optimal' will go to array part */
    }
  }
  lua_assert((optimal == 0 || optimal / 2 < na) && na <= optimal);
  *pna = na;
  return optimal;
}
  • 当 key 值在区间 [0, 2 ^ i] 的数量超过 2 ^ (i - 1) 时,就会使用 2 ^ i 作为数组的尺寸大小。也就是说,存入数组部分的数量,必须达到数组长度的 1/2 ,该数组长度才是合适的。因此,数组大小只会取能满足条件的区间部分对应的长度,不在区间内的,则全部存入到哈希表中,哈希表的长度,也就等于总数量减去存入数组部分的数量。
  • 可以看到,对数组部分的长度要求,可以保证数组部分使用率达到 50% 以上,同时,哈希表会在没有空闲位置时,才会进行扩容,而每一次数组扩容时,也会重新计算存入哈希表的数量,因此哈希表的使用率同样是保持 50% 以上。

示例

Table_1.png

  • 以 int 类型为例,一个 table 的存入过程如上图所示(红色为新加入的 key ,绿色为发生变动的 key):
      1. 对一个新建的 table ,设置 t[1] = 1 ,由于数组和哈希表都没有可存入位置,需要进行扩容。
      • 由于 key = 1 处于区间 [0, 2 ^ 0] ,数量为 1 ,大于 2 ^ (0 - 1) ,因此扩大数组长度为 1 。
      • 将 t[1] 存入数组部分 array[0]。
      1. 设置 t[2] = 2 ,由于数组和哈希表都没有可存入位置,需要进行扩容。
      • 由于 key = 2 处于区间 [0, 2 ^ 1] ,数量为 2 ,大于 2 ^ (1 - 1) ,因此扩大数组长度为 2 。
      • 将 t[2] 存入数组部分 array[1]。
      1. 设置 t[17] = 17 ,由于数组和哈希表都没有可存入位置,需要进行扩容。
      • 由于 key = 17 处于区间 [0, 2 ^ 5] ,数量为 3 ,小于 2 ^ (5 - 1) ,因此扩大哈希表长度为 1 。
      • 将 t[17] 存入哈希表 node[0]。
      1. 设置 t[9] = 9 ,由于数组和哈希表都没有可存入位置,需要进行扩容。
      • 由于 key = 9 处于区间 [0, 2 ^ 4] ,数量为 3 ,小于 2 ^ (4 - 1) ,因此扩大哈希表长度为 2 。
      • 原 node[0] 的 t[17] 的 key 值重新进行 hash 计算,结果为 1 ,因此 t[17] 存入 node[1]。
      • t[9] 的 key 值 hash 计算结果同样为 1 ,由于 node[1] 已经写入了数据,其 key 不相同,所以不能覆盖。
      • 由于 node[1] 的 key 值做 hash 计算还是为 1 ,即 node[1] 的数据和当前节点符合,是主节点。
      • 从后往前在 node 中查找空闲节点,找到 node[0] ,将 t[9] 存入 node[0] 中。
      • 设置 node 的链表指向,从 node[1] 指向 node[0] 。
      1. 设置 t[5] = 5 ,由于数组和哈希表都没有可存入位置,需要进行扩容。
      • 由于 key = 5 处于区间 [0, 2 ^ 3] ,数量为 3 ,小于 2 ^ (3 - 1) ,因此扩大哈希表长度为 4 。
      • 原 node[0] 的 t[9] 的 key 值重新进行 hash 计算,结果为 1 ,因此 t[9] 存入 node[1] 。
      • 原 node[1] 的 t[17] 的 key 值 hash 计算结果同样为 1 ,与第 4 步同理,因此 t[17] 存入 node[3] ,node[1] 指向 node[3] 。
      • t[5] 的 key 值 hash 计算结果也为 1 ,下一个空闲节点为 node[2] ,将 node[2] 指向 node[1] 的下一个节点,即 node[2] 指向 node[3] 。
      • t[5] 设置到到 node[2] 中,将主节点 node[1] 指向 node[2] ,而 node[2] 指向 node[3] ,链表中的元素数量增加。
      1. 设置 t[7] = 7 ,由于数组没有可存入位置,哈希表有空闲位置,所以不需要进行扩容。
      • t[7] 的 key 值 hash 计算结果为 3 ,由于 node[3] 已经写入了数据,其 key 不相同,所以不能覆盖。
      • 由于 node[3] 的 key = 17 ,做 hash 计算为 1 ,和当前节点不符合,因此找到下一个空闲节点 node[0] 。
      • 找到 node[3] 的主节点 node[1] ,遍历链表,找到 node[3] 的上一个节点 node[2] 。
      • 将 node[2] 指向 node[0] ,将 node[3] 的数据 t[17] 存入 node[0] 中。
      • 将 t[7] 存入 node[3] 中。
      1. 设置 t[6] = 6 ,由于数组和哈希表都没有可存入位置,需要进行扩容。
      • 由于 key = 6 处于区间 [0, 2 ^ 3] ,数量为 5 ,大于 2 ^ (3 - 1) ,因此扩大数组长度为 8 ,同时哈希表长度缩短为 2 。
      • 原 node[2] 和 node[3] 的 key 值小于数组长度,直接存入数组 array[4] 、array[6] 中。
      • 原 node[0] 和 node[1] 重新存入 node 中,node[1] 指向 node[0] 。
      • t[6] 直接存入数组部分 array[5] 中。

获取 table 长度

  • 在 lua 中获取 table 长度,通常使用 #t 返回。从 ‘#’ 转变成获取长度的过程主要代码如下:
// lparse.c

static UnOpr getunopr (int op) {
  switch (op) {
    case TK_NOT: return OPR_NOT;
    case '-': return OPR_MINUS;
    case '~': return OPR_BNOT;
    case '#': return OPR_LEN;
    default: return OPR_NOUNOPR;
  }
}
// lcode.h


typedef enum UnOpr { OPR_MINUS, OPR_BNOT, OPR_NOT, OPR_LEN, OPR_NOUNOPR } UnOpr;
// lcode.c

void luaK_prefix (FuncState *fs, UnOpr op, expdesc *e, int line) {
  static const expdesc ef = {VKINT, {0}, NO_JUMP, NO_JUMP};
  luaK_dischargevars(fs, e);
  switch (op) {
    case OPR_MINUS: case OPR_BNOT:  /* use 'ef' as fake 2nd operand */
      if (constfolding(fs, op + LUA_OPUNM, e, &ef))
        break;
      /* else */ /* FALLTHROUGH */
    case OPR_LEN:
      codeunexpval(fs, cast(OpCode, op + OP_UNM), e, line);
      break;
    case OPR_NOT: codenot(fs, e); break;
    default: lua_assert(0);
  }
}
// loprcodes.h

typedef enum {
/*----------------------------------------------------------------------
  name		args	description
------------------------------------------------------------------------*/
...

OP_UNM,/*	A B	R[A] := -R[B]					*/
OP_BNOT,/*	A B	R[A] := ~R[B]					*/
OP_NOT,/*	A B	R[A] := not R[B]				*/
OP_LEN,/*	A B	R[A] := length of R[B]				*/

...

} OpCode;
// lvm.c

void luaV_execute (lua_State *L, CallInfo *ci) {
  LClosure *cl;
  TValue *k;
  StkId base;
  const Instruction *pc;
  int trap;
#if LUA_USE_JUMPTABLE
#include "ljumptab.h"
#endif
 tailcall:
  trap = L->hookmask;
  cl = clLvalue(s2v(ci->func));
  k = cl->p->k;
  pc = ci->u.l.savedpc;
  if (trap) {
    if (cl->p->is_vararg)
      trap = 0;  /* hooks will start after VARARGPREP instruction */
    else if (pc == cl->p->code)  /* first instruction (not resuming)? */
      luaD_hookcall(L, ci);
    ci->u.l.trap = 1;  /* there may be other hooks */
  }
  base = ci->func + 1;
  /* main loop of interpreter */
  for (;;) {
    Instruction i;  /* instruction being executed */
    StkId ra;  /* instruction's A register */
    vmfetch();
    lua_assert(base == ci->func + 1);
    lua_assert(base <= L->top && L->top < L->stack + L->stacksize);
    /* invalidate top for instructions not expecting it */
    lua_assert(isIT(i) || (cast_void(L->top = base), 1));
    vmdispatch (GET_OPCODE(i)) {
      ...

      vmcase(OP_LEN) {
        Protect(luaV_objlen(L, ra, vRB(i)));
        vmbreak;
      }

      ...
  }
}
  • 可以看到,'#' 符号首先转换成 UnOpr.OPR_LEN 类型,再转换成 OpCode.OP_LEN 类型,最后根据 OpCode 类型,调用 luaV_objlen 方法获取长度。
// lvm.c

void luaV_objlen (lua_State *L, StkId ra, const TValue *rb) {
  const TValue *tm;
  switch (ttypetag(rb)) {
    case LUA_VTABLE: {
      Table *h = hvalue(rb);
      tm = fasttm(L, h->metatable, TM_LEN);
      if (tm) break;  /* metamethod? break switch to call it */
      setivalue(s2v(ra), luaH_getn(h));  /* else primitive len */
      return;
    }
    case LUA_VSHRSTR: {
      setivalue(s2v(ra), tsvalue(rb)->shrlen);
      return;
    }
    case LUA_VLNGSTR: {
      setivalue(s2v(ra), tsvalue(rb)->u.lnglen);
      return;
    }
    default: {  /* try metamethod */
      tm = luaT_gettmbyobj(L, rb, TM_LEN);
      if (unlikely(notm(tm)))  /* no metamethod? */
        luaG_typeerror(L, rb, "get length of");
      break;
    }
  }
  luaT_callTMres(L, tm, rb, rb, ra);
}
  • 对 table 类型,先检查其元表是否有 __len 方法,如果有,则调用该元方法返回 table 长度。如果没有,则需要通过 luaH_getn 方法来获取。
// ltable.c

lua_Unsigned luaH_getn (Table *t) {
  unsigned int limit = t->alimit;
  if (limit > 0 && isempty(&t->array[limit - 1])) {  /* (1)? */
    /* there must be a boundary before 'limit' */
    if (limit >= 2 && !isempty(&t->array[limit - 2])) {
      /* 'limit - 1' is a boundary; can it be a new limit? */
      if (ispow2realasize(t) && !ispow2(limit - 1)) {
        t->alimit = limit - 1;
        setnorealasize(t);  /* now 'alimit' is not the real size */
      }
      return limit - 1;
    }
    else {  /* must search for a boundary in [0, limit] */
      unsigned int boundary = binsearch(t->array, 0, limit);
      /* can this boundary represent the real size of the array? */
      if (ispow2realasize(t) && boundary > luaH_realasize(t) / 2) {
        t->alimit = boundary;  /* use it as the new limit */
        setnorealasize(t);
      }
      return boundary;
    }
  }
  /* 'limit' is zero or present in table */
  if (!limitequalsasize(t)) {  /* (2)? */
    /* 'limit' > 0 and array has more elements after 'limit' */
    if (isempty(&t->array[limit]))  /* 'limit + 1' is empty? */
      return limit;  /* this is the boundary */
    /* else, try last element in the array */
    limit = luaH_realasize(t);
    if (isempty(&t->array[limit - 1])) {  /* empty? */
      /* there must be a boundary in the array after old limit,
         and it must be a valid new limit */
      unsigned int boundary = binsearch(t->array, t->alimit, limit);
      t->alimit = boundary;
      return boundary;
    }
    /* else, new limit is present in the table; check the hash part */
  }
  /* (3) 'limit' is the last element and either is zero or present in table */
  lua_assert(limit == luaH_realasize(t) &&
             (limit == 0 || !isempty(&t->array[limit - 1])));
  if (isdummy(t) || isempty(luaH_getint(t, cast(lua_Integer, limit + 1))))
    return limit;  /* 'limit + 1' is absent */
  else  /* 'limit + 1' is also present */
    return hash_search(t, limit);
}
  • 检查 table 长度的流程为:
    • alimit 大于 0 ,并且 array[alimit - 1] 为空。
      • 如果 array[alimit - 2] 不为空,则 table 长度为 alimit - 1 。
        • 确定 table 长度后,如果 alimit 不为 array 真实长度(table 的 flags 的第 8 位为 1)或 alimit 是 2 的指数次幂,并且新的长度(alimit - 1)不为 2 的指数次幂,则设置 alimit 为新的长度,并且标记 table 的alimit 为非 array 真实长度。
      • 如果 array[alimit - 2] 为空,则进行二分查找,当 array[i] 为空,且 array[i - 1] 不为空时,i 即为 table 长度。
        • 确定 table 长度后,如果 alimit 不为 array 真实长度(table 的 flags 的第 8 位为 1)或 alimit 是 2 的指数次幂,并且新的长度大于 array 真实长度的一半时,设置 alimit 为新的长度,并且标记 table 的alimit 为非 array 真实长度。
    • alimit 为 0 或 array[alimit - 1] 不为空。
      • alimit 不是 array 的真实长度,并且 alimit 不是 2 的指数次幂。
        • array[alimit] 为空,则 table的长度为 alimit 。
        • array[alimit] 不为空。
          • array[array.length - 1] 为空,即 alimit 是旧的 table 长度,目前的 table 的长度处在 (alimit, array.length) 区间,使用二分查找得到 table 长度。
            • 将 alimit 更新为新的长度。
          • array[array.length - 1] 不为空。
            • 哈希表为空,或者哈希表不存在 array.length + 1 的 key,则 table 的长度为 array.length 。
            • 哈希表存在 array.length + 1 的 key ,则在哈希表中查找长度。
              • 确定 table 的长度所处区间(i, j)。
                • 设置 i = array.length ,j = array.length * 2 ,检查哈希表中是否存在 key 为 j 的节点。
                • 如果存在对应节点,则更新区间,i = j ,j = j * 2 ,继续查找,直到不存在对应节点或者 j 大于 2 ^ 30 。
                • 在区间(i, j)内,使用二分查找得到 table 的长度。
      • alimit 是 array 的真实长度或 alimit 是 2 的指数次幂。
        • 哈希表为空,或者哈希表不存在 array.length + 1 的 key,则 table 的长度为 array.length 。
        • 哈希表存在 array.length + 1 的 key ,则在哈希表中查找长度。
  • 可以看到,检查的流程有点复杂,其中有几个主要的点:
    • table 长度,是以 t[i] 存在且 t[i + 1] 不存在作为主要判断条件,即连续的常规数组能获取到正确的长度,而非连续的数组、键值对等情况则不能保证。
    • 只有数组部分的最后一个索引都有值时,才会去哈希表中继续查找,不然 table 的长度只会在 [0, array.length] 范围内。
    • 通常情况下,alimit 代表数组部分长度。获取 table 长度时,当 table 长度小于数组部分长度时,会修改 alimit 的值为 table 的当前长度,则下次获取长度时能快速返回,不需要每次都进行二分查找。
    • 修改 alimit 的值时,会限制在 (array.length / 2, array.length] 范围,因为 alimit 主要用于获取数组部分长度。因此,如果 table 的长度在 [0, array.length / 2] 内时,则每次都还是需要进行二分查找。也就是说,只有当数组部分的使用率在 50% 以上时,频繁获取 table 的长度才不会有额外的计算。

获取 table 的下一个值

  • 在 lua 中,通过 next 方法,可以获取 table 的下一个值,主要的代码实现为:
// ltable.c

int luaH_next (lua_State *L, Table *t, StkId key) {
  unsigned int asize = luaH_realasize(t);
  unsigned int i = findindex(L, t, s2v(key), asize);  /* find original key */
  for (; i < asize; i++) {  /* try first array part */
    if (!isempty(&t->array[i])) {  /* a non-empty entry? */
      setivalue(s2v(key), i + 1);
      setobj2s(L, key + 1, &t->array[i]);
      return 1;
    }
  }
  for (i -= asize; cast_int(i) < sizenode(t); i++) {  /* hash part */
    if (!isempty(gval(gnode(t, i)))) {  /* a non-empty entry? */
      Node *n = gnode(t, i);
      getnodekey(L, s2v(key), n);
      setobj2s(L, key + 1, gval(n));
      return 1;
    }
  }
  return 0;  /* no more elements */
}

static unsigned int findindex (lua_State *L, Table *t, TValue *key,
                               unsigned int asize) {
  unsigned int i;
  if (ttisnil(key)) return 0;  /* first iteration */
  i = ttisinteger(key) ? arrayindex(ivalue(key)) : 0;
  if (i - 1u < asize)  /* is 'key' inside array part? */
    return i;  /* yes; that's the index */
  else {
    const TValue *n = getgeneric(t, key);
    if (unlikely(isabstkey(n)))
      luaG_runerror(L, "invalid key to 'next'");  /* key not found */
    i = cast_int(nodefromval(n) - gnode(t, 0));  /* key index in hash table */
    /* hash elements are numbered after array ones */
    return (i + 1) + asize;
  }
}

static const TValue *getgeneric (Table *t, const TValue *key) {
  Node *n = mainpositionTV(t, key);
  for (;;) {  /* check whether 'key' is somewhere in the chain */
    if (equalkey(key, n))
      return gval(n);  /* that's it */
    else {
      int nx = gnext(n);
      if (nx == 0)
        return &absentkey;  /* not found */
      n += nx;
    }
  }
}
  • 通过 findindex 方法,查找指定的 key 值对应的下一个索引位置。
    • 如果 key 为 nil ,则从数组部分开始查找,下一个索引位置为 0 。
    • 如果 key - 1 小于数组长度,则 key 对应的值仍处于数组部分,下一个索引位置 key 。
    • 如果 key 大于数组长度,则 key 对应的值处于哈希表部分,找到存入的位置节点,下一个索引位置即为该节点在 node 中的索引值加 1,再加上数组的长度。
  • 根据得到的索引值,获取该位置对应的值,如果对应的值为空,则会返回 nil 。

迭代器访问 table

  • 迭代器访问 table ,即 for 遍历,代码结构如下:
  for val_1, val_2, ... , val_n in explist do
    block
  end
  • 上述代码等价于
  do
    local func, state, val = explist
    while true do
      local val_1, val_2, ... , val_n = func(state, val)
      if val_1 == nil then
        break
      end
      val = val_1
      block
    end
  • 其中,explist 为迭代器,只执行一次,返回迭代器函数 func 、状态 state 、迭代器初始值 val 。遍历过程即执行 func ,将返回的第一个参数 val_1 作为新的 val ,执行完 block 代码块后,再继续执行 func ,直到返回的第一个参数为空时停止。
  • table 的迭代器有两种,pairs 和 ipairs 。

pairs

  • pairs 方法的代码如下:
// lbaselib.c 

static int luaB_pairs (lua_State *L) {
  luaL_checkany(L, 1);
  if (luaL_getmetafield(L, 1, "__pairs") == LUA_TNIL) {  /* no metamethod? */
    lua_pushcfunction(L, luaB_next);  /* will return generator, */
    lua_pushvalue(L, 1);  /* state, */
    lua_pushnil(L);  /* and initial value */
  }
  else {
    lua_pushvalue(L, 1);  /* argument 'self' to metamethod */
    lua_call(L, 1, 3);  /* get 3 values from metamethod */
  }
  return 3;
}
  • 根据是否有元方法,pairs 会执行不同的流程。
    • 如果当前 table 没有元表,或元表没有元方法 __pairs ,则迭代器函数为 luaB_next ,状态为 table ,初始值为 nil ,即使用 next 方法来完成迭代访问过程。
    • 如果当前 table 有元表,并且有元方法 __pairs ,则将 table 作为参数,调用 __pairs 方法,并返回三个参数,对应迭代器函数、状态、初始值,从而完成迭代过程。
  • __pairs 的方法大致为:
  __pairs = function(table)
    local func = function(table, key)
      local nextKey = findNextKey()
      local nextValue = table[nextKey]
      return nextKey, nextValue
    end
    return func, table, nil
  end
  • 没有 __pairs 方法时,则使用 luaB_next 返回,其实现为:
// lbaselib.c

static int luaB_next (lua_State *L) {
  luaL_checktype(L, 1, LUA_TTABLE);
  lua_settop(L, 2);  /* create a 2nd argument if there isn't one */
  if (lua_next(L, 1))
    return 2;
  else {
    lua_pushnil(L);
    return 1;
  }
}
// lapi.c

LUA_API int lua_next (lua_State *L, int idx) {
  Table *t;
  int more;
  lua_lock(L);
  api_checknelems(L, 1);
  t = gettable(L, idx);
  more = luaH_next(L, t, L->top - 1);
  if (more) {
    api_incr_top(L);
  }
  else  /* no more elements */
    L->top -= 1;  /* remove key */
  lua_unlock(L);
  return more;
}
// ltable.c

int luaH_next (lua_State *L, Table *t, StkId key) {
  unsigned int asize = luaH_realasize(t);
  unsigned int i = findindex(L, t, s2v(key), asize);  /* find original key */
  for (; i < asize; i++) {  /* try first array part */
    if (!isempty(&t->array[i])) {  /* a non-empty entry? */
      setivalue(s2v(key), i + 1);
      setobj2s(L, key + 1, &t->array[i]);
      return 1;
    }
  }
  for (i -= asize; cast_int(i) < sizenode(t); i++) {  /* hash part */
    if (!isempty(gval(gnode(t, i)))) {  /* a non-empty entry? */
      Node *n = gnode(t, i);
      getnodekey(L, s2v(key), n);
      setobj2s(L, key + 1, gval(n));
      return 1;
    }
  }
  return 0;  /* no more elements */
}

static unsigned int findindex (lua_State *L, Table *t, TValue *key,
                               unsigned int asize) {
  unsigned int i;
  if (ttisnil(key)) return 0;  /* first iteration */
  i = ttisinteger(key) ? arrayindex(ivalue(key)) : 0;
  if (i - 1u < asize)  /* is 'key' inside array part? */
    return i;  /* yes; that's the index */
  else {
    const TValue *n = getgeneric(t, key);
    if (unlikely(isabstkey(n)))
      luaG_runerror(L, "invalid key to 'next'");  /* key not found */
    i = cast_int(nodefromval(n) - gnode(t, 0));  /* key index in hash table */
    /* hash elements are numbered after array ones */
    return (i + 1) + asize;
  }
}

ipairs

  • ipairs 方法的代码如下:
// lbaselib.c 

static int luaB_ipairs (lua_State *L) {
  luaL_checkany(L, 1);
  lua_pushcfunction(L, ipairsaux);  /* iteration function */
  lua_pushvalue(L, 1);  /* state */
  lua_pushinteger(L, 0);  /* initial value */
  return 3;
}
  • ipairs 的迭代器方法为 ipairsaux ,其代码为:
// lbaselib.c 

static int ipairsaux (lua_State *L) {
  lua_Integer i = luaL_checkinteger(L, 2) + 1;
  lua_pushinteger(L, i);
  return (lua_geti(L, 1, i) == LUA_TNIL) ? 1 : 2;
}
  • 迭代器的初始值为 0 ,迭代器函数会对传入的参数加 1 ,再获取 table 中的值并压入栈上。当 table 中存在对应 key 时,ipairs 会返回栈顶的两个值,即 key-value ,而当 table 中不存在对应 key 时,则只会返回一个值,即存放在栈顶的 value ,此时 value 为 nil 。因此,迭代器函数的返回值 val_1 即为 nil ,遍历停止。
  • 和 pairs 相比,ipairs 只能遍历 key 值为从 1 开始的连续范围,当某一个 key 值不存在时,则会中断,即只能遍历常规数组类型的 table 。

总结

  • table 是 lua 中重要的数据结构,同时具备数组和哈希表的功能,但却不是简单的数组部分对应数组存储,哈希表部分对应哈希表存储,基于内存优化做了一系列的处理。了解其中的实现细节,有助于更好地使用这种类型。