本周继续我们的 Lua 源码阅读,这次来到了 table
的源码。Lua 里的 table
可兼具 array
跟 hashmap
的功能,紧凑精悍。
初始化
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;
}
借助 luaM_new
从内存中分配出一块新内存,然后主要分为 array
数组部分与 node
哈希表部分分别进行初始化。
数组部分的初始化发生在 setarrayvector
函数中:
static void setarrayvector (lua_State *L, Table *t, int size) {
int i;
luaM_reallocvector(L, t->array, t->sizearray, size, TValue);
for (i=t->sizearray; i<size; i++)
setnilvalue(&t->array[i]);
// Expands to: (&t->array[i])->tt = LUA_TNIL;
t->sizearray = size;
}
流程很简单,先 realloc
数组部分的内存,调整到预期大小,然后逐项置为 nil
,并设置好 sizearray
,就完成了数组部分的初始化。
哈希表这一部分发生在 setnodevector
函数中:
static void setnodevector (lua_State *L, Table *t, int size) {
int lsize;
// 如果哈希表预期长度为 0,填入一个全局唯一的 dummynode
// 借助 dummynode,可以避免空表的特例判断,也可以避免 lsizenode 出现负值(2 ** 0 = 1)
if (size == 0) { /* no elements to hash part? */
t->node = cast(Node *, dummynode); /* use common `dummynode' */
lsize = 0;
}
else {
int i;
lsize = ceillog2(size);
if (lsize > MAXBITS)
luaG_runerror(L, "table overflow");
size = twoto(lsize);
// 以上几步,先进行 log2,然后再做 pow2
// 可以将 size 转换为比 size 大且为 2 的整数次幂中最小的数
// 备注:这里的 ceillog2 并不是标准的数学意义上的 log2,做了一点小变换
t->node = luaM_newvector(L, size, Node);
// 初始化每个哈希表成员
for (i=0; i<size; i++) {
Node *n = gnode(t, i);
// Expands to: (&(t)->node[i])
// 将当前节点的 next 指针置 NULL, 将 key/value 置 nil
gnext(n) = NULL;
setnilvalue(gkey(n));
setnilvalue(gval(n));
}
}
// 存放尺寸和lastfree指针指向最后一个元素
t->lsizenode = cast_byte(lsize);
// lastfree 指针被用于后边哈希表插入的逻辑中,指向最后一个空闲的哈希表节点
t->lastfree = gnode(t, size); /* all positions are free */
}
如果哈希表的预期大小为 0,则以一个全局结构体 dummynode
作为占位符填入 node
字段;否则先将 size
向上扩展到最邻近的 2 的整数次幂,然后分配内存,逐个初始化哈希表里的每一个节点。
经过初始化之后,一个空的 table
长这个样子:
数据结构
了解了初始化过程后,我们再回过头来看看 Lua 内部的数据结构:
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;
CommonHeader、TValue 在之前的文章已做过分析;数组、哈希表的存储主要涉及的四个字段分别为 array
、sizearray
,以及 node
、lsizenode
。(注意其中 lsizenode
比较特殊,是实际哈希表长度的 log2
。)
接着来看看组成哈希表的 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;
由 key
与 value
构成,其中 value
就是普通的 TValue
类型,而 key
是一个 union
。多了 nk
这一层套娃的结构体主要是为了提供 next
字段,用以将哈希表形成链,后文会阐述。
数组的插入
以这段代码为例:
local t = {}
t[1] = "1111"
t[2] = "2222"
t[3] = "3333"
t[4] = "4444"
其字节码为:
002B 0A000000 [1] newtable 0 0 0 ; array=0, hash=0
002F 09404080 [2] settable 0 256 257 ; 1 "1111"
0033 09C04081 [3] settable 0 258 259 ; 2 "2222"
0037 09404182 [4] settable 0 260 261 ; 3 "3333"
003B 09C04183 [5] settable 0 262 263 ; 4 "4444"
003F 1E008000 [6] return 0 1
我们分别分析一下几次 settable
背后的流程。
首次插入
进入到 settable
opcode 的对应逻辑:
void luaV_execute (lua_State *L, int nexeccalls) {
// ...
switch (GET_OPCODE(i)) {
// ...
case OP_SETTABLE: {
Protect(luaV_settable(L, ra, RKB(i), RKC(i)));
continue;
}
void luaV_settable (lua_State *L, const TValue *t, TValue *key, StkId val) {
int loop;
for (loop = 0; loop < MAXTAGLOOP; loop++) {
const TValue *tm;
if (ttistable(t)) { /* `t' is a table? */
// 首先获取它的hash部分
Table *h = hvalue(t);
TValue *oldval = luaH_set(L, h, key); /* do a primitive set */
if (!ttisnil(oldval) || /* result is no nil? */
(tm = fasttm(L, h->metatable, TM_NEWINDEX)) == NULL) { /* or no TM? */
// 替换原来的旧值
setobj2t(L, oldval, val);
luaC_barriert(L, h, val);
return;
}
/* else will try the tag method */
}
else if (ttisnil(tm = luaT_gettmbyobj(L, t, TM_NEWINDEX)))
luaG_typeerror(L, t, "index");
if (ttisfunction(tm)) {
callTM(L, tm, t, key, val);
return;
}
t = tm; /* else repeat with `tm' */
}
luaG_runerror(L, "loop in settable");
}
最终调用的是 luaH_set
函数,我们继续一路跟进去:
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);
}
}
luaH_get
是用来在 table
中执行查询的,后文会解析。先尝试用这个函数在表中查询,如果查到了,直接返回原有指针,然后由外部的调用者将旧值覆盖即可(可见上边的 luaV_settable
函数);如果未查到,则进入 newkey
流程,这里我们继续跟进去:
static TValue *newkey (lua_State *L, Table *t, const TValue *key) {
// 计算 key 在哈希表中的对应位置
Node *mp = mainposition(t, key);
// 如果该位置上已经有数据了,或者哈希表为空
if (!ttisnil(gval(mp)) || mp == dummynode) {
Node *othern;
// 尝试寻找一个空闲位置
Node *n = getfreepos(t); /* get a free place */
// 找不到空闲位置即进入 rehash 流程
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 */
}
// ... 省略
}
gkey(mp)->value = key->value; gkey(mp)->tt = key->tt;
luaC_barriert(L, t, key);
lua_assert(ttisnil(gval(mp)));
return gval(mp);
}
先借助 mainposition
计算出 key
应该对应到哈希表中的什么位置。由于哈希表目前还是空的(只存在一个 dummynode
),将进入 if
分支。但是现在哈希表还是实在太空,在 getfreepos
中也找不到合适的空闲位置:
static Node *getfreepos (Table *t) {
// while 的条件进不去,因为此时 t->lastfree == t->node
while (t->lastfree-- > t->node) {
if (ttisnil(gkey(t->lastfree)))
return t->lastfree;
}
return NULL; /* could not find a free place */
}
所以我们将进入 rehash
流程,在完成 rehash
之后,将重新调用 luaH_set
以完成这一次的 settable
操作。
由于 rehash
函数的代码比较难理解,我们就不详细分析代码了。简要来说,这个函数会计算出新的合适的数组大小与哈希表大小,然后调用 resize
函数来腾移数据。计算过程基于以下规则:先找到所有 key
为数字的元素,然后尝试调整数组大小 nasize = 2 ** n
,把 key
为数组的元素按序尝试放置到数组中,使得新数组的空置比例低于 50%,而且数组的大小要尽可能大。剩下的元素,就将被划分到哈希表中。
因此,在一个
table
的不同阶段下,对于同一个元素,其被放置于数组部分还是在哈希表部分是可能发生变化的。
接着我们进入 resize
函数:
static void resize (lua_State *L, Table *t, int nasize, int nhsize) {
int i;
int oldasize = t->sizearray;
int oldhsize = t->lsizenode;
Node *nold = t->node; /* save old hash ... */
// 如果数组发生了扩容,则进行 realloc,并将多出来的部分 set nil
if (nasize > oldasize) /* array part must grow? */
setarrayvector(L, t, nasize);
/* create new hash part with appropriate size */
setnodevector(L, t, nhsize);
// 如果数组发生了缩容,则遍历少掉的那一部分,插入到哈希表中
if (nasize < oldasize) { /* array part must shrink? */
t->sizearray = nasize;
/* re-insert elements from vanishing slice */
for (i=nasize; i<oldasize; i++) {
// 如果多出来的部分元素不为nil
if (!ttisnil(&t->array[i]))
setobjt2t(L, luaH_setnum(L, t, i+1), &t->array[i]);
}
/* shrink array */
luaM_reallocvector(L, t->array, oldasize, nasize, TValue);
}
/* re-insert elements from hash part */
// 由于哈希表的长度发生了改变,因此所有数据都需要重新插入一遍
for (i = twoto(oldhsize) - 1; i >= 0; i--) {
Node *old = nold+i;
// 将原来不为nil的元素重新插入hash中
if (!ttisnil(gval(old)))
setobjt2t(L, luaH_set(L, t, key2tval(old)), gval(old));
}
// 释放旧的hash部分
if (nold != dummynode)
luaM_freearray(L, nold, twoto(oldhsize), Node); /* free old array */
}
resize
函数负责真正地调整数组、哈希表的大小,以及数据的腾移。完成 resize
之后,我们将逐层原路返回,回到 newkey
中,调用 luaH_set
重新完成新元素的插入。此时,我们计算得到的数组长度与哈希表长度应该分别为 1
跟 0
,基于这个前提,我们重新回顾下 luaH_set
:
TValue *luaH_set (lua_State *L, Table *t, const TValue *key) {
// 此时 luaH_get 已经能成功从数组部分取得元素了
// 返回的是一个 nil 值
const TValue *p = luaH_get(t, key);
t->flags = 0;
// 但是那个 nil 值并非 luaO_nilobject 这个全局统一的 nil 实例
// (两个指针地址不同)
// 因此这里可以进入 cast 分支,直接将数组部分的第一个元素给递交了出去
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);
}
}
整个调用链路的示意图:
完成上述操作后,我们的表在内存里应当长这样:
其他几次插入
第二次插入时,sizearray
是 1
,仍然需要走一遍跟第一次一致的扩容过程。
第三次插入时,sizearray
是 2
,还是需要走一遍扩容过程,将数组部分扩容到 4
。
第四次插入时,sizearray
是 4
,无需扩容了,直接替换原本第 3
个 nil
元素即可。
初始化时插入
如果我们选择在声明变量时就已经将取值准备完毕,将进入另外一种稍微不同的流程里。如以下代码:
local t = {
"value 1",
"value 2",
"value 3",
"value 4",
}
002B 0A000002 [1] newtable 0 4 0 ; array=4, hash=0
002F 41000000 [2] loadk 1 0 ; "value 1"
0033 81400000 [3] loadk 2 1 ; "value 2"
0037 C1800000 [4] loadk 3 2 ; "value 3"
003B 01C10000 [5] loadk 4 3 ; "value 4"
003F 22400002 [6] setlist 0 4 1 ; index 1 to 4
0043 1E008000 [7] return 0 1
opcode 发生了变化。与之前不一样的是,这次是先将几个常量载入到寄存器中,然后调用 setlist
完成插入。setlist
简单来说,就是把从 1
到 B
的寄存器按顺序批量插入到 A
之中,C
是当前的批次编号(后续解析到虚拟机的部分我们再仔细理一理)。我们来看 setlist
的代码:
case OP_SETLIST: {
int n = GETARG_B(i); // to
int c = GETARG_C(i); // from
int last;
Table *h;
// ... 省略无关代码
runtime_check(L, ttistable(ra));
h = hvalue(ra);
last = ((c-1)*LFIELDS_PER_FLUSH) + n;
if (last > h->sizearray) /* needs more space? */
luaH_resizearray(L, h, last); /* pre-alloc it at once */
for (; n > 0; n--) {
TValue *val = ra+n;
setobj2t(L, luaH_setnum(L, h, last--), val);
luaC_barriert(L, h, val);
}
continue;
}
可以看到,首先是调用 luaH_resizearray
先把数组部分进行扩容,然后按顺序一个一个 luaH_setnum
。整个过程至多只会 resize
一次,十分高效。
如上文的分析,如果一个一个地插入,Lua 会发生比较频繁的
resize
与rehash
过程,性能损耗较大。所以,如果我们需要一个比较大的数组,最好不要从空表开始逐个插入,应该在创建的时候预先分配好空间(如使用一些特殊值进行占位),或者借助 LuaJIT 的 table.new 指定数组、哈希表部分的大小。
哈希的插入
首次插入
哈希表的插入流程也是自 settable
开始,然后依次进入到 luaV_settable
、luaH_set
、luaH_get
、newkey
、rehash
,到目前为止,都跟数组的插入没有什么区别,直到 rehash
里计算到的 nasize
与 nhsize
开始才产生差异。现在我们传递给 resize
的新的数组、哈希表大小中,数组大小无需变化,需要扩容的变成了哈希表:
static void resize (lua_State *L, Table *t, int nasize, int nhsize) {
int i;
int oldasize = t->sizearray;
int oldhsize = t->lsizenode;
Node *nold = t->node; /* save old hash ... */
// ... 省略
setnodevector(L, t, nhsize);
// ... 省略
/* re-insert elements from hash part */
// 由于哈希表的长度发生了改变,因此所有数据都需要重新插入一遍
for (i = twoto(oldhsize) - 1; i >= 0; i--) {
Node *old = nold+i;
// 将原来不为nil的元素重新插入hash中
if (!ttisnil(gval(old)))
setobjt2t(L, luaH_set(L, t, key2tval(old)), gval(old));
}
// 释放旧的hash部分
if (nold != dummynode)
luaM_freearray(L, nold, twoto(oldhsize), Node); /* free old array */
}
在扩容的同时也会做重新哈希、取模,目前我们的表还是空的,所以并没有什么特殊的事情发生。在 resize
结束之后,我们一路返回到 newkey
,然后还是与数组的首次插入一致——再次调用 luaH_set
真正完成这次的插入操作。由于哈希表已被扩容,这一次调用 luaH_set
函数的过程中,luaH_get
能取到对应哈希表上的 nil
了,然后还是一样往上层返回,最终在 luaV_settable
函数中将 nil
取代为新的取值。
目前我们的 table
在内存里长这样:
其他几次插入
第二次插入时,又发生了一遍扩容,然后还是老套路,扩容后重新调用 luaH_set
顺利完成插入;
第三次插入时,又发生了一遍扩容,哈希表的大小被扩大到 4
,然后还是一样的老套路;
如此依次按 2
的幂次进行倍增。如果哈希表中数字类型的 key
比较多,也有可能发生数组的扩容,伴随着哈希表被缩容。在哈希表缩容的过程中,多出来的元素会重新调用 luaH_set
,如果小于 sizearray
,就会进入到数组部分之中。
在上述每一次插入时,都有可能发生哈希冲突,下面我们来具体讨论讨论哈希冲突的解决流程。为了描述简便,我们假设哈希表冲突发生在第四次插入时。这时候哈希表的大小为 4
,不会发生扩容,我们只需要专注于如何解决冲突。
假设我们的 table
当前状态如下:
注意:由于哈希算法的存在,几个元素在这是乱序的。
假设元素 4 本来按照哈希算法应该插入到第四个位置,但是从图中可以看到,这个位置已经被元素 3 给占用了,产生了哈希冲突。相应的处理流程发生在 newkey
函数里:
static TValue *newkey (lua_State *L, Table *t, const TValue *key) {
// 计算 key 在哈希表中的对应位置
Node *mp = mainposition(t, key);
// 如果该位置上已经有数据了,或者哈希表为空
if (!ttisnil(gval(mp)) || mp == dummynode) {
Node *othern;
// 尝试寻找一个空闲位置
Node *n = getfreepos(t); /* get a free place */
// 找不到空闲位置即进入 rehash 流程
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 */
}
此时 mp
计算出来即为元素 3,不是 nil
,因此进入 if 分支,先尝试借助 getfreepos
找到一个空闲的位置:
static Node *getfreepos (Table *t) {
while (t->lastfree-- > t->node) {
if (ttisnil(gkey(t->lastfree)))
return t->lastfree;
}
return NULL; /* could not find a free place */
}
lastfree
在 setnodevector
时被设置为最后一个元素,也就是元素 3,很明显它不是空的,因此继续往上查找。刚好往上探了一次就找到了,于是此处返回第三个元素(目前还是 nil
)。得到这个可用的空闲节点后,我们继续 newkey
的流程:
// ...
lua_assert(n != dummynode);
othern = mainposition(t, key2tval(mp));
// 如果目前占用我们的那个元素,自身也是被其他元素占用了原本该有的位置
if (othern != mp) { /* is colliding node out of its main position? */
// 那么将占用我们的元素整个挪到 freepos,然后自己鸠占鹊巢
/* 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 */
// 那么将自己放到 freepos 上,并链到已有元素的后边
/* 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);
}
如注释所描述,分两种场景。概况起来,就是发生「一次冲突」时,使用类似线性散列探测的方法,从 freenode
开始,从后往前查找空闲节点,并将新元素链接到冲突节点的后边;「二次冲突(或更多)」时,不再追加成链的长度,而是直接把已成链的冲突节点挪开,自己站上自己本来就该有的位置。这样可以有效避免整个哈希表在频繁冲突时退化为非常长的链表。
我们假定目前只产生了「一次冲突」,也就是元素 1、2、3 都没有发生冲突,都在其原本该在的位置上。这种情况下,我们的新元素将被放置到 freenode
上,并链在自己原本该有的位置之后。经过哈希冲突处理后,我们的 table
内部状态应该长这样:
查询
啃完了插入这块硬骨头,剩下的功能都相对轻松。对于查询操作而言,逻辑很简单,最外层的入口为 luaH_get
函数:
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 */
// 什么情况下会走串这条 case 语句呢
// 就是在 key 是数字,但又不是整数的时候,如 table[3.1415] = "chenxy.me"
}
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;
}
}
}
核心就是根据 key
的类型进行分发,分别走到 luaH_getstr
与 luaH_getnum
,其他情况下就进入 default
流程。
default
流程与 luaH_getstr
逻辑基本一致,就是计算哈希,然后在哈希表中查找。如果发现哈希表对应节点的 key
不一致,而且又存在 next
字段,说明插入时发生了哈希冲突,需要依次在链上往后查找,直到此链结束未找到,或者中途找到了预期的 key
,返回对应的 value
。
对于 luaH_getnum
函数,代码如下:
const TValue *luaH_getnum (Table *t, int key) {
/* (1 <= key && key <= t->sizearray) */
// 只要比 sizearray 小,那么都在数组部分
if (cast(unsigned int, key-1) < cast(unsigned int, t->sizearray))
return &t->array[key-1];
// 否则在 hash 部分中
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;
}
}
如果小于 sizearray
,直接从数组部分取值。否则就从哈希表部分取值。注意到仍然需要处理哈希冲突的场景,代码与上边 default
、luaH_getstr
都是类似的。
长度计算
Lua 的长度计算,也就是 #t
并不完美,特别是表中存在 nil
时,很容易被糊弄。原因可以从代码中分析得到:
int luaH_getn (Table *t) {
unsigned int j = t->sizearray;
if (j > 0 && ttisnil(&t->array[j - 1])) {
// 如果数组的最后一个元素为 nil,那么在数组中二分查找「最后」一个 nil
/* there is a boundary in the array part: (binary) search for it */
unsigned int i = 0;
while (j - i > 1) {
unsigned int m = (i+j)/2;
if (ttisnil(&t->array[m - 1])) j = m;
else i = m;
}
return i;
}
// 如果数组部分的最后一个元素为非 nil……
/* else must find a boundary in hash part */
else if (t->node == dummynode) /* hash part is empty? */
// ……而且哈希表是空的,那么非常简单,返回数组大小即可
return j; /* that is easy... */
// ……否则,在哈希表中(类似)二分查找「最后」一个 nil
else return unbound_search(t, j);
}
分三种情况处理:
- 如果数组的最后一个元素为
nil
,则在数组部分二分查找「最后」一个nil
-- 用这段代码来糊弄 Lua 吧!
local t = {}
t[1] = "yes"
t[3] = "yes"
t[4] = "yes"
t[5] = "yes"
t[7] = "yes"
print(#t) -- 将输出 5
-- 可以借助 gdb 确认 sizearray 确实是 8:
-- $1 = {next = 0x555555598f00, tt = 5 '\005', marked = 1 '\001', flags = 0 '\000', lsizenode = 0 '\000', metatable = 0x0, array = 0x555555597ef0,
node = 0x555555582ea0 <dummynode_>, lastfree = 0x555555582ea0 <dummynode_>, gclist = 0x4014000000000000, sizearray = 8}
- 如果数组的最后一个元素非
nil
,而且哈希表是空的,则返回数组大小
-- 用这段代码来糊弄 Lua 吧!
local t = {}
t[1] = "yes"
t[3] = "yes"
t[4] = "yes"
t[5] = "yes"
t[8] = "yes"
print(#t) -- 将输出 8
-- 可以借助 gdb 确认 sizearray 确实是 8
- 如果数组的最后一个元素非
nil
,而且哈希表非空,那么在哈希表中「二分」查找最后一个nil
-- 用这段代码来糊弄 Lua 吧!
local t = {}
t[1] = "yes"
t[2] = "yes"
t["yes"] = "no"
print(#t) -- 将输出 2
总结
日常使用 Lua 表的过程中,可以多注意以下几点:
- 数组部分与哈希表部分,默认都是按照
2
的幂次进行扩容、缩容; - 如果要构造一个大
table
,不管是数组部分还是哈希表部分,最好借助table.new
等工具进行预先分配; - 在一个
table
的整个生命周期中,同一个元素可能有时在数组部分,有时又在哈希表部分; table
的长度计算很容易出现非预期结果,如果需要计算table
的长度,最好不要出现nil
值,也不要在数组部分出现「空洞」;table
的长度计算在绝大多数情况下是有代价的,时间复杂度大约为O(log N)
;
参考资料
- 《Lua设计与实现》;
- https://github.com/lichuang/Lua-5.1.4-codedump
题图来自:Photo by 五玄土 ORIENTO on Unsplash
Pingback引用通告: Lua opcode 解析 | chenxy.me