Lua table源码分析

来源:互联网 发布:淘宝一键复制软件 编辑:程序博客网 时间:2024/06/05 16:27

一、table结构

1Table结构体

首先了解一下table结构的组成结构,table是存放在GCObject里的。结构如下:

typedefstruct Table {

  CommonHeader;

  lu_byte flags;  /* 1<<p means tagmethod(p) is not present */

  lu_bytelsizenode;  /*2lsizenode次方作为哈希表长度 */

  struct Table*metatable/*元表 */;

  TValue *array/*数组 */

  Node *node; /*哈希表 */

  Node *lastfree;  /*指向最后一个为闲置的链表空间 */

  GCObject *gclist;   ????这个是做什么用的

  intsizearray; /*数组的大小 */

} Table;

table的结构可以看出,table在设计的时候以两种结构来存放数据。一般情况对于整数key,会用array来存放,而其它数据类型key会存放在哈希表上。并且用lsizenode作为链表的长度,sizearray作为数组长度。

table 分为三块内存,分别是table结构,数组,hash数组。hash数组大小为 2 n次幂,所以最小是1.lua进行了优化定义了一个空的hashdummynode,所有的空的table表的hash node指针都指向这个dummynode表。dummynode是只读的所以没有线程安全问题

 

#define dummynode         (&dummynode_)

static const Node dummynode_ = {

 {NILCONSTANT},  /* value */

 {{NILCONSTANT, 0}}  /* key */

};

 

 

2Node结构体

typedefunion TKey {
  struct {
    TValuefields;
    struct Node *next;  /* 指向下一个冲突node */
  } nk;
  TValue tvk;
} TKey;
// TKey保存了key的值和指向冲突时下一个节点的指针
 
typedefstruct Node {
  TValue i_val;
  TKey i_key;
} Node;

Node结构很好理解,就是一个键值对的结构。主要是TKey结构,这里用了union,所以TKey的大小是nk的大小。并且实际上TValueTValuefields是同一个结构,因此tvknkTValuefields都是代表键值。而且这里有一个链表结构struct Node *next,用于指向下一个有冲突的node

 

二、创建table

table的创建通过lua_newtable函数实现。通过定位具体实现是在luaH_new这个函数进行table的创建。代码如下:

 Table *luaH_new (lua_State *L) {
  GCObject *o = luaC_newobj(L, LUA_TTABLE, sizeof(Table));   //创建一个table对象并加入全局gclist
  Table *t = gco2t(o);
  t->metatable = NULL;  //设置元表为nil
  t->flags = cast_byte(~0);
  t->array = NULL;   //设置数组部分为nil
  t->sizearray = 0;  
  setnodevector(L, t, 0);  //设置hash表部分
  return t;
}

主要是对table进行初始化, setnodevector是对hash表大小进行设置,具体代码如下:

static void setnodevector (lua_State *L, Table *t, unsigned int size)
{
  if (size == 0)  //hash 部分为nil hash指针指向那个常量的空表
{  /* 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);  //计算ceillog2size))函数用数组模拟log2()的实现
    if (lsize > MAXHBITS)
      luaG_runerror(L, "table overflow");
    size = twoto(lsize);   //分配的hash大小一定是2的整数幂求实际应该分配的大小
    t->node = luaM_newvector(L, size, Node);   //分配hash表空间
    for (i = 0; i < (int)size; i++) {//遍历hash表将每个节点的keyvalue赋值为nil
      Node *n = gnode(t, i);
      gnext(n) = 0;
      setnilvalue(wgkey(n));
      setnilvalue(gval(n));
    }
    t->lsizenode = cast_byte(lsize);
t->lastfree = gnode(t, size);       //记录最后一个空闲位置的索引
}
}

判断hash部分是不是空,是指向常量节点,计算实际分配的hash表空间大小,分陪内存,初始化keyvaluenil,保存hash表大小,和最后的空闲节点索引。

三、释放table

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

 

这里就是调用Lua内存管理函数,释放table结构,数组,hash表这三块内存。

 

四、重置table数组和hash表大小

static void rehash (lua_State *L, Table *t, const TValue *ek) {
  unsigned int asize;  
  unsigned int na;  
  unsigned int nums[MAXABITS + 1]; //每个区间整数key的个数
  int i;
  int totaluse;                     //hash + array 元素个数的
  for (i = 0; i <= MAXABITS; i++) nums[i] = 0;  /* reset counts */
  na = numusearray(t, nums);  /* 计算数组部分每个区间key个数和数组总元素个数*/
  totaluse = na;  /* all those keys are integer keys */
  totaluse += numusehash(t, nums, &na); /*计算hash部分每个区间key个数和数组总元素个数*/
  /* count extra key */
  na += countint(ek, nums);      //计算新加入的key 
  totaluse++;
  /* compute new size for array part */
  asize = computesizes(nums, &na);    //计算数组部分应该分配多少空间asize 总大小na 已有元素大小
  /* resize the table to new computed sizes */
  luaH_resize(L, t, asize, totaluse - na);   //重新分配空间
}

rehash 主要是统计当前有多少个有效的键值,并计算出数组部分需要开辟多大的空间。要保证数组空间的利用率为50%以上。再调用luaH_resize重新分配table的数组和hash表的大小。

void luaH_resize (lua_State *L, Table *t, unsigned int nasize,
                                          unsigned int nhsize) 
{
  unsigned int i;
  int j;
  unsigned int oldasize = t->sizearray;  //保存old数组size
  int oldhsize = allocsizenode(t);  //保存old hash size
  Node *nold = t->node;  /* 保存老的hash表地址 */
  if (nasize > oldasize)  /* 是不是数组扩大了 */
    setarrayvector(L, t, nasize);  //分配新的数组并清空超过的部分
  /* 创建新的hash*/
  setnodevector(L, t, nhsize);
  if (nasize < oldasize) {  /* 数组部分缩减了吗*/
    t->sizearray = nasize;
    /* re-insert elements from vanishing slice */
    for (i=nasize; i<oldasize; i++) {  //将超出的部分保存在hash表中
      if (!ttisnil(&t->array[i]))
        luaH_setint(L, t, i + 1, &t->array[i]);  
    }
    /* 分配新的数组*/
    luaM_reallocvector(L, t->array, oldasize, nasize, TValue);
  }
  /* 从后向前遍历将老的值放到新hash表里 */
  for (j = oldhsize - 1; j >= 0; j--) {
    Node *old = nold + j;
    if (!ttisnil(gval(old))) {
      /* doesn't need barrier/invalidate cache, as entry was
         already present in the table */
      setobjt2t(L, luaH_set(L, t, gkey(old)), gval(old));
    }
  }
  if (oldhsize > 0)  /* not the dummy node? */
luaM_freearray(L, nold, cast(size_t, oldhsize)); /* free old hash */
}

数组扩容,重新非配数组空间,并清空超出部分。数组缩减,将超出部分的内容,插入到hash表中。

五、table插入

通过luaH_new创建的Table开始实际上是一张空表,只是包含了Table本身的数据结构。创建完以后需要添加元素,

添加到函数为:luaH_set,该函数在设置前会根据key的值去获取对应的Value,如果能找到,则直接设置,如果找不到,则需要申请一个位置来存放(Key,Value)luaH_set 返回key所对应值的地址,然后通过调用setobjt2t 设置键所对应的值

TValue *luaH_set (lua_State *L, Table *t, const TValue *key) {
  const TValue *p = luaH_get(t, key);
  if (p != luaO_nilobject)
    return cast(TValue *, p);
  else return luaH_newkey(L, t, key);
}

 

 

 

六、创建table hash表键

 

TValue *luaH_newkey (lua_State *L, Table *t, const TValue *key) {
  Node *mp;
  TValue aux;
 //如果keyfloat并且可以装换为int 则将其转换为int
  if (ttisnil(key)) luaG_runerror(L, "table index is nil");
  else if (ttisfloat(key)) {
    lua_Integer k;
    if (luaV_tointeger(key, &k, 0)) {  /* does index fit in an integer? */
      setivalue(&aux, k);
      key = &aux;  /* insert it as an integer */
    }
    else if (luai_numisnan(fltvalue(key)))
      luaG_runerror(L, "table index is NaN");
  }
  mp = mainposition(t, key);   //获取key hash表中的主位置
  if (!ttisnil(gval(mp)) || isdummy(t)) {  //主位置已经存在元素或者是空hash
    Node *othern;
    Node *f = getfreepos(t);  /* 获取空闲位置 */
    if (f == NULL) {  /* 没有空位置   */
      rehash(L, t, key);  /* 重构hash */
      /* 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, gkey(mp));  //获取这个位置上key的主位置(注意这里存放的元素主位置不一定就在这,有可能是冲突后放置在这个位置的元素)
    if (othern != mp) {  /* 这个位置的元素主位置不在这里也就是当前节点放置的是冲突节点*/
      /* other 是当前主位置上的冲突节点应当放置的位置 map是冲突节点当前放置的位置
         这里遍历查找map位置前一个冲突节点的位置 */
      while (othern + gnext(othern) != mp)  /* */
        othern += gnext(othern);
//移动mp位置上的元素将新key放置在这个位置上
      gnext(othern) = cast_int(f - othern);  /* */
      *f = *mp;  /* */
      if (gnext(mp) != 0) {
        gnext(f) += cast_int(mp - f);  /* correct 'next' */
        gnext(mp) = 0;  /* now 'mp' is free */
      }
      setnilvalue(gval(mp));
    }
    else {  //当前节点的主位置就在这里那么将新key放在freepos 关联相关next
      if (gnext(mp) != 0)
        gnext(f) = cast_int((mp + gnext(mp)) - f);  
      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);  //返回 node value 地址
}

 

键值的插入流程: 
 

 

(1)首先判断key是不是空,为空的话出错 
(2)
调用mainposition找到主位置mp,主位置即keyhash映射到表中的位置 
(3)
如果主位置为空,那么直接将key放到主位置。如果主位置不为空,那么先通过getfreepos函数找到一个空闲的位置n(这个位置从末尾开始找)。如果找不到空闲位置,就要调用rehash重新调整hash表,调整好后调用luaH_set设置。如果找到空闲位置,计算mp的主位置othern。如果两个不相等,即mp的主位置不在mp指向的这里,那么mp移到n,并将n放在mp的主位置链上。如果两个相等,即mp的主位置就是这里,将key赋值给n,然后将n放到mp的主位置链上。

这里简单的模拟一下,素组代表hash表,采用取模方式获得hash位置,Last指向最后空余位置。 
 
加入1417 
 
加入27,发现主位置7不为空,判断17的主位置也在7,将27赋给位置0,将17next指向0

 
加入30,返现0位置不为空,同时27的主位置不在0,将27移到最后的lastfree,将30赋给0位置。

 
加入15 
 
加入25,发现主位置5不为空,15的主位置也在5,将25赋给lastfree,将15next指向位置8

七、table 迭代(lua_nextfindindex

 int luaH_next (lua_State *L, Table *t, StkId key) {
  unsigned int i = findindex(L, t, key);  /*找到key在数组的索引如果keyhash表中那么返回hash表中的索引 +  sizearray */
  for (; i < t->sizearray; i++) {  /* try first array part */
if (!ttisnil(&t->array[i])) {  /* a non-nil value? */
//key 是栈顶位置这里将下一个索引放入栈栈并没有增加只是覆盖了上一个key的值
      setivalue(key, i + 1);  
//value 压入栈栈会在这个函数的外部增长   
      setobj2s(L, key+1, &t->array[i]); 
      return 1; 
    }
  }
 //注意这里当I == sizearray 是证明数组遍历结束0开始遍历hash部分压入hash第一个元素的key 和第一个元素的值
  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;  /* 返回0没有元素了 */

 

 
static unsigned int findindex (lua_State *L, Table *t, StkId key) {
  unsigned int i;
  if (ttisnil(key)) return 0;  /* 如果key为空则返回第一个0 代表开始迭代*/
  i = arrayindex(key);
  if (i != 0 && i <= t->sizearray)  /* 查找数组部分 */
    return i;  /* yes; that's the index */
  else {
    int nx;
    Node *n = mainposition(t, key);   //计算key的主位置索引
    for (;;) {  /* 从主位置开始依次遍历和其冲突的节点 */
      /* 比较key的值是否相等 */
      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 */
        /*相等返回key的位置索引*/
        return (i + 1) + t->sizearray;
      }
      nx = gnext(n);
      if (nx == 0)
        luaG_runerror(L, "invalid key to 'next'");  /* key not found */
      else 
         n += nx;  //查找下一个冲突节点
    }
  }
 
static unsigned int arrayindex (const TValue *key) {
  if (ttisinteger(key)) {
    lua_Integer k = ivalue(key);
    if (0 < k && (lua_Unsigned)k <= MAXASIZE)
      return cast(unsigned int, k);
  }
  return 0;  /* 'key' did not match some condition */
}
 

luaH_next 是内部api  外部调用接口lua_next lua_next 实现如下

 
LUA_API int lua_next (lua_State *L, int idx) {  //idxtable在栈中的索引
  StkId t;
  int more;
  lua_lock(L);
  t = index2adr(L, idx);   //获取table指针
  api_check(L, ttistable(t));
  more = luaH_next(L, hvalue(t), L->top - 1);   //从栈中取出一个key 压入下一个key value 遍历完返回
  if (more) {
    api_incr_top(L); //top++
  }
  else  /* no more elements */
    L->top -= 1;  /* remove key */   遍历完移除最后一个key 
  lua_unlock(L);
  return more;

C 遍历 table 代码:

// 进行下面步骤前先将 table 压入栈顶 
 int nIndex = lua_gettop( pLua );  // 取 table 索引值 
 lua_pushnil( pLua );  // nil 入栈作为初始 key 
 while( 0 != lua_next( pLua, nIndex ) ) 
 
     
// 现在栈顶(-1)是 value-2 位置是对应的 key 
     // 
这里可以判断 key 是什么并且对 value 进行各种处理 
     lua_pop( pLua, 1 );  // 弹出 value,让 key 留在栈顶 
 } 
 // 现在栈顶是 table

lua_next() 这个函数的工作过程是:
1)
先从栈顶弹出一个 key
2)
从栈指定位置的table里取下一对key-value,先将 key入栈再将 value 入栈
3)
如果第 2 步成功则返回非 0 值,否则返回 0,并且不向栈中压入任何值

2 步中从 table里取出所谓下一对 key-value”是相对于第 1步中弹出的 key的。table里第一对 key-value的前面没有数据,所以先用 lua_pushnil()压入一个 nil 充当初始 key

 

原创粉丝点击