Lua中table类型源码分析

Lua中的数据结构—table

lua定义了以下几种类型:

/*
** basic types
*/
#define LUA_TNONE       (-1)

#define LUA_TNIL        0
#define LUA_TBOOLEAN        1
#define LUA_TLIGHTUSERDATA  2
#define LUA_TNUMBER     3
#define LUA_TSTRING     4
#define LUA_TTABLE      5
#define LUA_TFUNCTION       6
#define LUA_TUSERDATA       7
#define LUA_TTHREAD     8

tables的实现被分成了两个部分: 核心由ltable.c完成,提供了table的基本存取方法, 外部table库(ltablib.c)提供了辅助操作接口(concat, foreach, foreachi, getn, maxn, insert, remove, setn, sort).

ltable.c table的主要代码在这里
lobject.h lua的基础对象的定义,包括table

直接上

一.table的结构定义:

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

这里做几个说明:

1.其中lu_byte定义:

luatypedef unsigned char lu_byte;

2.CommonHeader:
为所有可回收资源提供标记头

#define CommonHeader    GCObject *next; lu_byte tt; lu_byte marked

3.metatable是元表,作用请自行谷歌:
struct Table *metatable;

4.TValue

typedef struct lua_TValue {
  TValuefields;
} TValue;

TValuefields:
#define TValuefields    Value value; int tt;

Value:
typedef union {
  GCObject *gc;
  void *p;
  lua_Number n;
  int b;
} Value;

其实TValue是一个结构体,里面是TValuefields,它由一个宏定义完成,包括Value和一个int值。
Value是一个联合,它是里面数据类型的一种,可以是指针、数字等,这里不介绍具体用法。

5.Node

typedef union TKey {
  struct {
    TValuefields;
    struct Node *next;  /* for chaining */
  } nk;
  TValue tvk;
} TKey;

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

每个Node都是一个键值对。
lua之所以用这种方式来表示Node,感觉一个很重要的原因是为了适应不同参数类型的接口。
TKey中tvk是这个key的值,nk中的next则指向key冲突的下一个节点。lua的hash表的hash算法比较特别,一般的hash表都是根 据key算出hash(key),然后把这个key放在hash表的hash(key)位置上,如果有冲突的话,就放在hash(key)位置的链表上。
但是lua的hash表中,如果有冲突的话,lua会找hash表中一个空的位置(从后往前找,假设为x),然后把新的key放在这个空的位置x上,并且 让hash表中hash(key)处的节点的nk.next指向x。这个意思就是,假如有冲突的话,不用重新分配空间来存储冲突的key,而是利用 hash表上未用过的空格来存储。但是这样会引入另外一个问题,本来key是不应该放在x的,假设有另外一个key2,hash(key2)算出来的位置 也在x的话,那就表示本来x这个位置应该是给key2的,但是由于x被key占用了,导致key2没地方放了。这时候lua的处理方式是把key放到另外 一个空格,然后让key2占回x。当hash表已经没有空格的时候,lua就会resize这个hash表。这样做的好处主要是不用动态申请内存空 间,hash表初始化的时候有多少内存空间就用多少,不够就resize这个hash表。

小结

实际上,lsizenode,node,lastfree这三个属性存储的就是哈希表部分;array和sizearray存储的就是数组部分。数组部分比较简单,array就是数组本身,sizearray是数组的大小;哈希表部分的话,node指向哈希表起始地址,lsizenode是log2(node指向的哈希表节点数目),注意2^lsizenode不等于哈希表中存储变量的数目,因为哈希表是有可能有冲突的,所以一个哈希表节点是一个链表的表头,他可能对应多个存储变量。lastfree指向node里面最后一个未用的节点(这个用法很特别,后面会详细说)。

二.table的创建:

Table *luaH_new (lua_State *L, int narray, int nhash) {
  Table *t = luaM_new(L, 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;
}

lua创建新表的时候先为新表结构体分配内存(luaM_new(L, Table))。把新表link到global_state的gc上,并设置标志位。接下来 初始化表结构(node属性的终止符是一个dummynode)。调用setarrayvector()为表的数组项分配内存,调用 setnodevector()为表的节点项分配内存。最后返回表指针。 可以看出,array和hash部分默认都是0,然后用narray和nhash来初始化array和hash部分。array部分就初始化为narray长度的数组,hash部分就初始化为2^ceil(log2(nhash))长度的哈希表。table的哈希表的长度必须是2的幂,至于增长规则会在后面说到。

三.table的删除:

void luaH_free (lua_State *L, Table *t) {
  if (t->node != dummynode)
    luaM_freearray(L, t->node, sizenode(t), Node);
  luaM_freearray(L, t->array, t->sizearray, TValue);
  luaM_free(L, t);
}

如果表有节点项,释放,释放数组项,释放表头结构。

四.table的查找:

#define gnext(n)    ((n)->i_key.nk.next)

gnext(n)实现遍历node链表

const TValue *luaH_get (Table *t, const TValue *key) {
  switch (ttype(key)) {
    case LUA_TNIL: return luaO_nilobject;
    case LUA_TSTRING: return luaH_getstr(t, rawtsvalue(key));
    case LUA_TNUMBER: {
      int k;
      lua_Number n = nvalue(key);
      lua_number2int(k, n);
      if (luai_numeq(cast_num(k), nvalue(key))) /* index is int? */
        return luaH_getnum(t, k);  /* use specialized version */
      /* else go through */
    }
    default: {
      Node *n = mainposition(t, key);
      do {  /* check whether `key' is somewhere in the chain */
        if (luaO_rawequalObj(key2tval(n), key))
          return gval(n);  /* that's it */
        else n = gnext(n);
      } while (n);
      return luaO_nilobject;
    }
  }
}

对table进行查找时,对key进行判断,空则返回空,字符串则调用luaH_getstr(t, rawtsvalue(key)),数字则根据其是否为整数调用 luaH_getnum(t, k),否则,则计算出key主位置(mainposition()),遍历table的node节点,找到键对应的所在节点,返回该节点。 luaH_getstr和luaH_getnum其实也是这个过程,只不过对string和number的哈希算法不同,number也有可能会放在数组 部分而已。

1.若key是一个字符串类型LUA_TSHRSTR,则调用函数luaH_getstr来查找。其代码如下(ltable.c):

const TValue *luaH_getstr (Table *t, TString *key) {
  Node *n = hashstr(t, key);
  do {  /* check whether `key' is somewhere in the chain */
    if (ttisstring(gkey(n)) && rawtsvalue(gkey(n)) == key)
      return gval(n);  /* that's it */
    else n = gnext(n);
  } while (n);
  return luaO_nilobject;
}

若key为字符串,则根据hashstr(t,key)方法找到该键对应的node位置,遍历链表,返回对应的值。

2.若key是一个数字类型LUA_TNUMBER并且是一个int类型,则调用luaH_getint函数去查找。代码如下(ltable.c):

const TValue *luaH_getnum (Table *t, int key) {
  /* (1 <= key && key <= t->sizearray) */
  if (cast(unsigned int, key-1) < cast(unsigned int, t->sizearray))
    return &t->array[key-1];
  else {
    lua_Number nk = cast_num(key);
    Node *n = hashnum(t, nk);
    do {  /* check whether `key' is somewhere in the chain */
      if (ttisnumber(gkey(n)) && luai_numeq(nvalue(gkey(n)), nk))
        return gval(n);  /* that's it */
      else n = gnext(n);
    } while (n);
    return luaO_nilobject;
  }
}

若该key为大于等于1,小于等于数组长度的整数,则从数组里取出键对应的值,否则利用hashnum(t,nk)方法找到key对应的node位置,遍历链表,返回对应的值。

3.对应其他类型,也就是不是nil、整型和短字符串类型,都是计算hash值,然后在链表中去查找(因为拥有相同哈希值的冲突键值对,在哈希表中由Node的next成员链接起来了)。

这里要强调一个函数mainposition

static Node *mainposition (const Table *t, const TValue *key) {
  switch (ttype(key)) {
    case LUA_TNUMBER:
      return hashnum(t, nvalue(key));
    case LUA_TSTRING:
      return hashstr(t, rawtsvalue(key));
    case LUA_TBOOLEAN:
      return hashboolean(t, bvalue(key));
    case LUA_TLIGHTUSERDATA:
      return hashpointer(t, pvalue(key));
    default:
      return hashpointer(t, gcvalue(key));
  }
}

可见table中对不同类型数据有不同的hash算法,这里我想再看看table使用的几种hash函数
1.hashnum

static Node *hashnum (const Table *t, lua_Number n) {
  unsigned int a[numints];
  int i;
  if (luai_numeq(n, 0))  /* avoid problems with -0 */
    return gnode(t, 0);
  memcpy(a, &n, sizeof(a));
  for (i = 1; i < numints; i++) a[0] += a[i];
  return hashmod(t, a[0]);
}

这是在查找key为数字而非1-arraysize大小内或非整数时调用的hash算法,其中lua_Number是个double型,这里是把n的高低4个字节相加,作为取模的对象。

2.hashstr、hashboolean、hashpointer等

#define hashpow2(t,n)      (gnode(t, lmod((n), sizenode(t))))
#define hashstr(t,str)  hashpow2(t, (str)->tsv.hash)
#define hashboolean(t,p)        hashpow2(t, p)
/*
** for some types, it is better to avoid modulus by power of 2, as
** they tend to have many 2 factors.
*/
#define hashmod(t,n)    (gnode(t, ((n) % ((sizenode(t)-1)|1))))
#define hashpointer(t,p)    hashmod(t, IntPoint(p))

这里就不赘述了,有兴趣的可以继续深入。

四.table的赋值:

TValue *luaH_set (lua_State *L, Table *t, const TValue *key) {
  const TValue *p = luaH_get(t, key);
  t->flags = 0;
  if (p != luaO_nilobject)
    return cast(TValue *, p);
  else {
    if (ttisnil(key)) luaG_runerror(L, "table index is nil");
    else if (ttisnumber(key) && luai_numisnan(nvalue(key)))
      luaG_runerror(L, "table index is NaN");
    return newkey(L, t, key);
  }
}

它首先查找key是否在table中,若在,则直接替换原来的值,否则调用luaH_newkey,插入新的(key,value)。

函数luaH_newkey代码如下(ltable.c):

static TValue *newkey (lua_State *L, Table *t, const TValue *key) {
  Node *mp = mainposition(t, key);
  if (!ttisnil(gval(mp)) || mp == dummynode) {
    Node *othern;
    Node *n = getfreepos(t);  /* get a free place */
    if (n == NULL) {  /* cannot find a free place? */
      rehash(L, t, key);  /* grow table */
      return luaH_set(L, t, key);  /* re-insert key into grown table */
    }
    lua_assert(n != dummynode);
    othern = mainposition(t, key2tval(mp));
    if (othern != mp) {  /* is colliding node out of its main position? */
      /* yes; move colliding node into free position */
      while (gnext(othern) != mp) othern = gnext(othern);  /* find previous */
      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));
    }
    else {  /* colliding node is in its own main position */
      /* new node will go into free position */
      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);
}

往table中插入新的值,其基本思路是检测key的主位置(main position)是否为空,这里主位置就是key的哈希值在node数组中(哈希表)的位置。若主位置为空,则直接把相应的(key,value)插入 到这个node中。若主位置被占了,检查占领该位置的(key,value)的主位置是不是在这个地方,若不在这个地方,则移动占领该位置的 (key,value)到一个新的空node中,并且把要插入的(key,value)插入到相应的主位置;若在这个地方(即占领该位置的 (key,value)的主位置就是要插入的位置),则把要插入的(key,value)插入到一个新的,空node中。若找不到空闲位置放置新键值,则 进行rehash函数,扩增加或减少哈希表的大小找出新位置,然后再调用luaH_set把要插入的(key,value)到新的哈希表中,直接返回 LuaH_set的结果。

static void rehash (lua_State *L, Table *t, const TValue *ek) {
  int nasize, na;
  int nums[MAXBITS+1];  /* nums[i] = number of keys between 2^(i-1) and 2^i */
  int i;
  int totaluse;
  for (i=0; i<=MAXBITS; 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 */
  resize(L, t, nasize, totaluse - na);
}

rehash首先统计当前table中到底有value值不是nil的键值对的个数,然后根据这个数值确定table中数组部分的大小(其大小保证数组部分的空间利用率必须50%),最后调用luaH_resize函数来重建table。
具体过程是首先调用函数numusearray计算table中数组部分非nil的数值的个数,然后调用numusehash函数计算table中哈希部 分的非nil的键值对的个数。调用countint函数来确定将要插入的(key,value)是否可以放在数组部分,接着调用computesizes 来计算新的table数组部分的大小,最后调用luaH_resize函数根据原来table中数据构建新的table。

五.table的遍历:

table的遍历分为ipairs和pairs,ipairs遍历数组部分,pairs遍历整个table。ipairs遍历顺序就是从0开始一次加1往后遍历table的数组部分。pairs的遍历实际上是调用luaH_next:

int luaH_next (lua_State *L, Table *t, StkId key) {
  int i = findindex(L, t, key);  /* find original element */
  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));
      setobj2s(L, key+1, &t->array[i]);
      return 1;
    }
  }
  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 */
}

算法先查数组部分,然后查哈希表,找到当前key的下一个key。所以遍历的结果是先遍历数组部分,然后遍历哈希表部分,哈希表部分实际上就是把node顺序遍历一次。

六.总结:

(1)在对table操作时,尽量不要触发rehash操作,因为这个开销是非常大的。在对table插入新的键值对时(也就是说key原来不在 table中),可能会触发rehash操作,而直接修改已存在key对于的值,不会触发rehash操作的,包括赋值为nil。

(2)在遍历一个table时,不允许向table插入一个新键,否则将无法预测后续的遍历行为,但lua允许在遍历过程中,修改table中已存在的键对应的值,包括修改后的值为nil,也是允许的。

(3)table中要想删除一个元素等同于向对应key赋值为nil,等待垃圾回收。但是删除table一个元素时候,并不会触发表重构行为,即不会触发rehash操作。

(4)为了减少rehash操作,当构造一个数组时,如果预先知道其大小,可以预分配数组大小。在脚本层可以使用local t = {nil,nil,nil}来预分配数组大小。在C语言层,可以使用接口void lua_createtable (lua_State *L, int narr, int nrec);来预分配数组大小。

(5)注意在使用长度操作符#对数组其长度时,数组不应该包含nil值,否则很容易出错。

print(#{1,nil})  --1  
print(#{1,nil,1}) --3  
print(#{1,nil,1,nil}) --1  
posted @ 2016-08-24 10:55  何人之名  阅读(1117)  评论(1编辑  收藏  举报