local t = {100}
t[2] = 200
table.insert(t, 300)
t.x = 9.2
print(t[1], t[2], t[3], t.x)
其可能的实现方式如下图:
注意数组部分不需要保存键值,只要在底层实现时才考虑这种区别,其他情况,即使对虚拟机来说,访问表项也是由底层自动统一的,因为使用的时候无须考虑这种差别。在往表中插入数值时,表会根据key-value的值和表当前的数据内容自动动态地使用这个两个部分:数据部分试图保存所有key值介于1和某个上限n之间的值,非整数key和超过数据范围n的整数key对应的值将存入哈希表部分。
对于数组部分,要求的数组的大小同时满足:1到n之间至少一半的空间被利用(避免像稀疏数组一样浪费空间);并且n/2+1到n之间的空间至少有一个空间被利用(避免n/2个空间就能容纳所有数据时申请了n个空间浪费)。
对于哈希部分,解决冲突的方式是用开放寻址法(open addressing),即所有的元素都存在哈希表中,使用这种方法往往可以让Hash表内数据更紧凑,有更高效的空间利用率,并且在用这个方法时还做了改进,下面将通过源代码具体分析。
3、源码实现
首先来看对应的数据结构Table,其源码如下(lobject.h):
105 #define TValuefields Value value_; int tt_
544 /*
545 ** Tables
546 */
547
548 typedef union TKey {
549 struct {
550 TValuefields;
551 struct Node *next; /* for chaining */
552 } nk;
553 TValue tvk;
554 } TKey;
555
556
557 typedef struct Node {
558 TValue i_val;
559 TKey i_key;
560 } Node;
561
562
563 typedef struct Table {
564 CommonHeader;
565 lu_byte flags; /* 1<<p means tagmethod(p) is not present */
566 lu_byte lsizenode; /* log2 of size of `node' array */
567 struct Table *metatable;
568 TValue *array; /* array part */
569 Node *node;
570 Node *lastfree; /* any free position is before this position */
571 GCObject *gclist;
572 int sizearray; /* size of `array' array */
573 } Table; Table结构的头CommonHeader与TString中是一样的,用于GC,实质上所有GC类型的头上相同的,都包含这个宏。368 Table *luaH_new (lua_State *L) {
369 Table *t = &luaC_newobj(L, LUA_TTABLE, sizeof(Table), NULL, 0)->h;
370 t->metatable = NULL;
371 t->flags = cast_byte(~0);
372 t->array = NULL;
373 t->sizearray = 0;
374 setnodevector(L, t, 0);
375 return t;
376 }
其中函数luaC_newobj(lgc.c中定义)用来创建一个新的可回收对象,并把创建的对象放到GC的链表中。lua中所有的可回收对象都是调用这个接口来创建的,方便后面GC回收。其中setnodevector用来初始化table的哈希表部分,初始值哈希表大小为1,并且node指向一个静态全局变量dummynode_,而不是NULL,这样做的目的是减少操作表时的判断操作。
I、查找
首先来看怎么在table中查找一个值,即t[key]操作时所发生的,代码如下(ltable.c):
478 /*
479 ** main search function
480 */
481 const TValue *luaH_get (Table *t, const TValue *key) {
482 switch (ttype(key)) {
483 case LUA_TNIL: return luaO_nilobject;
484 case LUA_TSHRSTR: return luaH_getstr(t, rawtsvalue(key));
485 case LUA_TNUMBER: {
486 int k;
487 lua_Number n = nvalue(key);
488 lua_number2int(k, n);
489 if (luai_numeq(cast_num(k), nvalue(key))) /* index is int? */
490 return luaH_getint(t, k); /* use specialized version */
491 /* else go through */
492 }
493 default: {
494 Node *n = mainposition(t, key);
495 do { /* check whether `key' is somewhere in the chain */
496 if (luaV_rawequalobj(gkey(n), key))
497 return gval(n); /* that's it */
498 else n = gnext(n);
499 } while (n);
500 return luaO_nilobject;
501 }
502 }
503 } 从代码可以看出查找的步骤是:463 /*
464 ** search function for short strings
465 */
466 const TValue *luaH_getstr (Table *t, TString *key) {
467 Node *n = hashstr(t, key);
468 lua_assert(key->tsv.tt == LUA_TSHRSTR);
469 do { /* check whether `key' is somewhere in the chain */
470 if (ttisshrstring(gkey(n)) && eqshrstr(rawtsvalue(gkey(n)), key))
471 return gval(n); /* that's it */
472 else n = gnext(n);
473 } while (n);
474 return luaO_nilobject;
475 } 443 /*
444 ** search function for integers
445 */
446 const TValue *luaH_getint (Table *t, int key) {
447 /* (1 <= key && key <= t->sizearray) */
448 if (cast(unsigned int, key-1) < cast(unsigned int, t->sizearray))
449 return &t->array[key-1];
450 else {
451 lua_Number nk = cast_num(key);
452 Node *n = hashnum(t, nk);
453 do { /* check whether `key' is somewhere in the chain */
454 if (ttisnumber(gkey(n)) && luai_numeq(nvalue(gkey(n)), nk))
455 return gval(n); /* that's it */
456 else n = gnext(n);
457 } while (n);
458 return luaO_nilobject;
459 }
460 }如果key的值小于等于数组大小,则直接返回相应的值,否则去哈希表中去查找。510 TValue *luaH_set (lua_State *L, Table *t, const TValue *key) {
511 const TValue *p = luaH_get(t, key);
512 if (p != luaO_nilobject)
513 return cast(TValue *, p);
514 else return luaH_newkey(L, t, key);
515 } 它首先查找key是否在table中,若在,则直接替换原来的值,否则调用luaH_newkey,插入新的(key,value)。忘table中插入新的值,其基本思路是检测key的主位置(main position)是否为空,这里主位置就是key的哈希值在node数组中(哈希表)的位置。若主位置为空,则直接把相应的(key,value)插入到这个node中。若主位置被占了,检查占领该位置的(key,value)的主位置是不是在这个地方,若不在这个地方,则移动占领该位置的(key,value)到一个新的空node中,并且把要插入的(key,value)插入到相应的主位置;若在这个地方(即占领该位置的(key,value)的主位置就是要插入的位置),则把要插入的(key,value)插入到一个新的空node中。399 ** inserts a new key into a hash table; first, check whether key's main
400 ** position is free. If not, check whether colliding node is in its main
401 ** position or not: if it is not, move colliding node to an empty place and
402 ** put new key in its main position; otherwise (colliding node is in its main
403 ** position), new key goes to an empty position.
404 */
405 TValue *luaH_newkey (lua_State *L, Table *t, const TValue *key) {
406 Node *mp;
407 if (ttisnil(key)) luaG_runerror(L, "table index is nil");
408 else if (ttisnumber(key) && luai_numisnan(L, nvalue(key)))
409 luaG_runerror(L, "table index is NaN");
410 mp = mainposition(t, key);
411 if (!ttisnil(gval(mp)) || isdummy(mp)) { /* main position is taken? */
412 Node *othern;
413 Node *n = getfreepos(t); /* get a free place */
414 if (n == NULL) { /* cannot find a free place? */
415 rehash(L, t, key); /* grow table */
416 /* whatever called 'newkey' take care of TM cache and GC barrier */
417 return luaH_set(L, t, key); /* insert key into grown table */
418 }
419 lua_assert(!isdummy(n));
420 othern = mainposition(t, gkey(mp));
421 if (othern != mp) { /* is colliding node out of its main position? */
422 /* yes; move colliding node into free position */
423 while (gnext(othern) != mp) othern = gnext(othern); /* find previous */
424 gnext(othern) = n; /* redo the chain with `n' in place of `mp' */
425 *n = *mp; /* copy colliding node into free pos. (mp->next also goes) */
426 gnext(mp) = NULL; /* now `mp' is free */
427 setnilvalue(gval(mp));
428 }
429 else { /* colliding node is in its own main position */
430 /* new node will go into free position */
431 gnext(n) = gnext(mp); /* chain new position */
432 gnext(mp) = n;
433 mp = n;
434 }
435 }
436 setobj2t(L, gkey(mp), key);
437 luaC_barrierback(L, obj2gco(t), key);
438 lua_assert(ttisnil(gval(mp)));
439 return gval(mp);
440 }上面的函数的执行过程如下:387 static Node *getfreepos (Table *t) {
388 while (t->lastfree > t->node) {
389 t->lastfree--;
390 if (ttisnil(gkey(t->lastfree)))
391 return t->lastfree;
392 }
393 return NULL; /* could not find a free place */
394 }它从lastfree处开始查找,查找第一个空的位置,其中成员lastfree保存的是哈希表中最后一个空的位置。因此它依次往前查找。343 static void rehash (lua_State *L, Table *t, const TValue *ek) {
344 int nasize, na;
345 int nums[MAXBITS+1]; /* nums[i] = number of keys with 2^(i-1) < k <= 2^i */
346 int i;
347 int totaluse;
348 for (i=0; i<=MAXBITS; i++) nums[i] = 0; /* reset counts */
349 nasize = numusearray(t, nums); /* count keys in array part */
350 totaluse = nasize; /* all those keys are integer keys */
351 totaluse += numusehash(t, nums, &nasize); /* count keys in hash part */
352 /* count extra key */
353 nasize += countint(ek, nums);
354 totaluse++;
355 /* compute new size for array part */
356 na = computesizes(nums, &nasize);
357 /* resize the table to new computed sizes */
358 luaH_resize(L, t, nasize, totaluse - na);
359 } rehash首先统计当前table中到底有value值不是nil的键值对的个数,然后根据这个数值确定table中数组部分的大小(其大小保证数组部分的空间利用率必须50%),最后调用luaH_resize函数来重建table。169 int luaH_next (lua_State *L, Table *t, StkId key) {
170 int i = findindex(L, t, key); /* find original element */
171 for (i++; i < t->sizearray; i++) { /* try first array part */
172 if (!ttisnil(&t->array[i])) { /* a non-nil value? */
173 setnvalue(key, cast_num(i+1));
174 setobj2s(L, key+1, &t->array[i]);
175 return 1;
176 }
177 }
178 for (i -= t->sizearray; i < sizenode(t); i++) { /* then hash part */
179 if (!ttisnil(gval(gnode(t, i)))) { /* a non-nil value? */
180 setobj2s(L, key, gkey(gnode(t, i)));
181 setobj2s(L, key+1, gval(gnode(t, i)));
182 return 1;
183 }
184 }
185 return 0; /* no more elements */
186 }
首先调用findindex获得开始检索的位置(比如,通常从等于key的位置开始查找),然后因此查找table中的数组部分和哈希部分的第一个非nil的位置。
4、总结
(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参考资料
Lua 5.2.1源码
http://blog.aliyun.com/787 Lua数据结构 — Table(三)
http://www.codingnow.com/temp/readinglua.pdf 《Lua源码欣赏》(云风)
原文地址:http://blog.csdn.net/maximuszhou/article/details/45047191