Lua的数据结构

1.Table的几个小知识点

  • table使用关联数组,我们可以使用任意类型的值来作为数组的索引(不能使用nil)
  • table没有固定的大小,可以动态的添加元素

2.table的构造

思考一下:如果现在定义了一个table a,将table a赋值给table b,此时它们的内存情况是什么样呢?
ab都会指向同一个内存块,如果a设置为nil,b依旧能访问该内存块的元素,直到b设置为nil后,Lua的垃圾回收机制会清理相应的内存。所以当b在更改table内的值后,a再去访问的时候,值也是改变的。

 

3. 从table结构体分析table属性

 1 typedef struct Table {
 2   CommonHeader;
 3   lu_byte flags;  /* 1<<p means tagmethod(p) is not present */ 
 4   lu_byte lsizenode;  /* 以2的lsizenode次方作为哈希表长度 */
 5   struct Table *metatable /* 元表 */;
 6   TValue *array;  /* 数组 */
 7   Node *node; /* 哈希表 */
 8   Node *lastfree;  /* 指向最后一个为闲置的链表空间 */
 9   GCObject *gclist;
10   int sizearray;  /* 数组的大小 */
11 } Table;
  • CommonHeader 为所有可回收资源提供标记头
  • lu_byte其实是 typedef unsigned char lu_byte,lu_byte flags用于表示表中提供了哪些元方法(元方法是什么?我们下面和元表一起解释)
  • lu_byte lsizenode 它的值就是表的长度,但是散列表大小的扩增一定是2的幂,如果散列桶数组要扩展的话,也是每次在原大小的基础上乘以2的形式扩展。
  • struct Table *metatable 也就是元表。
  • TValue *array是指向数组的指针
  • Node *node 是指向该表的散列桶数组起始位置的指针
TValue是一个结构体,里面保存着TValuefields,它是一个宏定义,包含value和int值,而value是一个联合体,它可以是指针、数字等等。
 1 typedef struct lua_TValue {
 2   TValuefields;
 3 } TValue;
 4 
 5 TValuefields:
 6 #define TValuefields    Value value; int tt
 7 
 8 Value:
 9 typedef union {
10   GCObject *gc;
11   void *p;
12   lua_Number n;
13   int b;
14 } Value;
3.2 Node类型
 1 typedef union TKey {
 2   struct {
 3     TValuefields;
 4     struct Node *next;  /* for chaining */
 5   } nk;
 6   TValue tvk;
 7 } TKey;
 8 
 9 typedef struct Node {
10   TValue i_val;
11   TKey i_key;
12 } Node;

 

每个Node都是一个键值对,里面包含了key和value。tvk是key的值,但是当我们出现hash冲突,此时lua的hash算法比较特殊,一般情况下,我们的hash算法都是根据key算出hash,然后如果有冲突的话,就放在改位置的链表上。而lua不同,当它出现hash冲突的时候,会在hash表中找到一个空的位置x,来存放key,并且让冲突处的节点的nk.next指向x。
这意味着什么呢?发生冲突我们无需重新分配空间来存储冲突的key,而是利用hash表上未用过的空白处来存储。刚才我们将key放在了空位置x,如果此时存在另一个key2,它算出的hash值是空位置x,而我们刚才的key只是因为hash冲突了,占用了其他key的位置,这个时候我们就讲key2放在x上,将key再放到另一个空白处。

3.3 table的实现

lua的table其实由数组段和hash两部分组成,当你的key值不会过于离散的时候,lua就会将它存储在数组段(也就是下图的array),反正会存储在hash段(也就是下图的node),这个分割线是以数组段的利用率不低于50%为准。hash段采用闭散列的算法,它将有冲突的key存储在空闲槽中,而不额外分配内存。

在我们table结构体中,array和sizearray都是表示数组段。
而lsizenode和node,lastfree是表示hash段。node指向hash表的起始位置,lsizenode是log2(node指向的哈希表的节点数目),lastfree指向node里面最后一个未使用的节点(因为我们在hash冲突的时候,是从后往前查找未使用的节点,lastfree存储最后一个未使用节点就可以方便查找)

如果此时hash表中已经没有空格了,那么lua就会resize这个hash表(等会再谈lua的动态扩增)

4. table的创建、查找、动态分配

4.1 table的创建
lua创建新表的时候先为新表分配内存Table * t = luaM_new(L, Table),然后将表连接到gc上并设置标志位luaC_link(L, obj2gco(t), LUA_TTABLE),然后初始化一些必要的属性,使用setarrayvector为数组段分配内存,setnodevector为hash部分分配内存,最后返回表指针。
 1 Table *luaH_new (lua_State *L, int narray, int nhash) {
 2   Table *t = luaM_new(L, Table);
 3   luaC_link(L, obj2gco(t), LUA_TTABLE);
 4   t->metatable = NULL;
 5   t->flags = cast_byte(~0);
 6   /* temporary values (kept only if some malloc fails) */
 7   t->array = NULL;
 8   t->sizearray = 0;
 9   t->lsizenode = 0;
10   t->node = cast(Node *, dummynode);
11   setarrayvector(L, t, narray);
12   setnodevector(L, t, nhash);
13   return t;
14 }
4.2 table的查找
table的查找会根据key进行判断,如果key为空就直接返回空,key为字符串就调用luaH_getstr(t, rawtsvalue(key)),key为数字则根据它是否整数调用luaH_getnum(t, k),否则,计算出key的位置,遍历table的node节点,找到对应键所在的节点返回。
 1 const TValue *luaH_get (Table *t, const TValue *key) {
 2   switch (ttype(key)) {
 3     case LUA_TNIL: return luaO_nilobject;
 4     case LUA_TSTRING: return luaH_getstr(t, rawtsvalue(key));
 5     case LUA_TNUMBER: {
 6       int k;
 7       lua_Number n = nvalue(key);
 8       lua_number2int(k, n);
 9       if (luai_numeq(cast_num(k), nvalue(key))) /* index is int? */
10         return luaH_getnum(t, k);  /* use specialized version */
11       /* else go through */
12     }
13     default: {
14       Node *n = mainposition(t, key);
15       do {  /* check whether `key' is somewhere in the chain */
16         if (luaO_rawequalObj(key2tval(n), key))
17           return gval(n);  /* that's it */
18         else n = gnext(n);
19       } while (n);
20       return luaO_nilobject;
21     }
22   }
23 }

因为key为数字比较特殊,所以研究一把luaH_getnum函数的实现

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

如果key大于等于1,小于数组的长度,则从数组中取出对应的键值,否则利用hashnum找到key对应的node位置,遍历node链表,返回对应的值

4.3 table的赋值
 1 static TValue *newkey (lua_State *L, Table *t, const TValue *key) {
 2   Node *mp = mainposition(t, key);
 3   if (!ttisnil(gval(mp)) || mp == dummynode) {
 4     Node *othern;
 5     Node *n = getfreepos(t);  /* get a free place */
 6     if (n == NULL) {  /* cannot find a free place? */
 7       rehash(L, t, key);  /* grow table */
 8       return luaH_set(L, t, key);  /* re-insert key into grown table */
 9     }
10     lua_assert(n != dummynode);
11     othern = mainposition(t, key2tval(mp));
12     if (othern != mp) {  /* is colliding node out of its main position? */
13       /* yes; move colliding node into free position */
14       while (gnext(othern) != mp) othern = gnext(othern);  /* find previous */
15       gnext(othern) = n;  /* redo the chain with `n' in place of `mp' */
16       *n = *mp;  /* copy colliding node into free pos. (mp->next also goes) */
17       gnext(mp) = NULL;  /* now `mp' is free */
18       setnilvalue(gval(mp));
19     }
20     else {  /* colliding node is in its own main position */
21       /* new node will go into free position */
22       gnext(n) = gnext(mp);  /* chain new position */
23       gnext(mp) = n;
24       mp = n;
25     }
26   }
27   gkey(mp)->value = key->value; gkey(mp)->tt = key->tt;
28   luaC_barriert(L, t, key);
29   lua_assert(ttisnil(gval(mp)));
30   return gval(mp);
31 }
dummynode是带头结点的指针。
往table中插入新值,先检测key的主位置(main position)是否为空,主位置就是key的哈希值在node中的位置。
如果主位置为空,就直接插入,主位置不为空,检查占领该位置的key的主位置是不是在这个地方,如果不在,则将该key移动到其他空闲位置,将要插入的key插入到这个位置中。如果在这个地方,则将要插入的key插入到一个空槽中。
如果找不到空闲位置放新键值,就rehash函数,扩增hash表的大小,再找出新位置,再调用luaH_set把要插入的key插入到新的哈希表中,直接返回LuaH_set的结果。
4.4 table的动态增长
4.4.1 rehash
 1 //加入key,重新分配hash与array的空间
 2 static void rehash (lua_State *L, Table *t, const TValue *ek) {
 3   int nasize, na;//nasize前期累计整数key个数,后期做为数组空间大小,na表示数组不为nil的个数
 4   int nums[MAXBITS+1];  /* 累计各个区间整数key不为nil的个数,包括hash, 例如nums[i]表示累计在[2^(i-1),2^i]区间内的整数key个数*/
 5   int i;
 6   int totaluse;//记录所有已存在的键,包括hash和array,即table里的成员个数
 7   for (i=0; i<=MAXBITS; i++) nums[i] = 0;  /* 初始化所有计数区间*/
 8   nasize = numusearray(t, nums);  /* 以区间统计数组里不为nil的个数,并获得总数*/
 9   totaluse = nasize;  /* all those keys are integer keys */
10   totaluse += numusehash(t, nums, &nasize);  /* 统计hash表里已有的键,以及整数键的个数已经区间分布*/
11   //如果新key是整数类型的情况
12   nasize += countint(ek, nums);
13   //累计新key
14   totaluse++;
15   /* 重新计算数组空间 */
16   na = computesizes(nums, &nasize);
17   /* 重新创建内存空间, nasize为新数组大小
18 //totaluse - na表示所有键的个数减去新数组的个数,即为新hash表需要存放的个数 */
19   resize(L, t, nasize, totaluse - na);
20 }

4.2 resize

 1 static void resize (lua_State *L, Table *t, int nasize, int nhsize) {
 2   int i;
 3   int oldasize = t->sizearray;
 4   int oldhsize = t->lsizenode;
 5   Node *nold = t->node;  /* 保存当前的hash表,用于后面创建新hash表时,可以重新对各个node赋值*/
 6   if (nasize > oldasize)  /* 是否需要扩展数组 */
 7     setarrayvector(L, t, nasize);
 8   /* 重新分配hash空间*/
 9   setnodevector(L, t, nhsize);  
10   if (nasize < oldasize) {  /* 小于之前大小,即有部分整数key放到了hash里 */
11     t->sizearray = nasize;
12     /* 超出部分存放到hash表里*/
13     for (i=nasize; i<oldasize; i++) {
14       if (!ttisnil(&t->array[i]))
15         setobjt2t(L, luaH_setnum(L, t, i+1), &t->array[i]);
16     }
17     /* 重新分配数组空间,去掉后面溢出部分*/
18     luaM_reallocvector(L, t->array, oldasize, nasize, TValue);
19   }
20   /* 从后到前遍历,把老hash表的值搬到新表中*/
21   for (i = twoto(oldhsize) - 1; i >= 0; i--) {
22     Node *old = nold+i;
23     if (!ttisnil(gval(old)))
24       setobjt2t(L, luaH_set(L, t, key2tval(old)), gval(old));
25   }
26   //释放老hash表空间
27   if (nold != dummynode)
28     luaM_freearray(L, nold, twoto(oldhsize), Node);  /* free old array */
29 }

 


作者:凉拌姨妈好吃
链接:https://www.jianshu.com/p/56ca3d77c7de
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
posted @ 2020-02-20 14:16  暗月梦影  阅读(421)  评论(0)    收藏  举报