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