参考书籍:《Lua设计与实现》
作者书籍对应Github:https://github.com/lichuang/Lua-Source-Internal
- Lua版本:5.3.5
概述
- Lua表分为数组和散列表部分,散列表可以存储不能存放在数组部分的数据,唯一的要求是键值不能为nil。
// lobject.h
typedef struct Table {
CommonHeader;
lu_byte flags; /* 1<<p means tagmethod(p) is not present */
lu_byte lsizenode; /* log2 of size of 'node' array */
unsigned int sizearray; /* size of 'array' array */
TValue *array; /* array part */
Node *node;
Node *lastfree; /* any free position is before this position */
struct Table *metatable;
GCObject *gclist;
} Table;
- Common Header :要进行 标记-清除 GC操作的数据类型。
- lu_byte flags :这是一个byte类型的数据,用于表示这个表中提供了哪些元方法。最开
始这个flags是空的,也就是0 ,当查找一次之后,如果该表中存在某个元方法,那么将
该元方法对应的flag bit置为l ,这样下一次查找时只需要比较这个b it 就行了。每个元
方法对应的bit定义在ltm. h 文件中。 - lu_byte lsizenode :该表中以2 为底的散列表大小的对数值。同时由此可知,散列表部分
的大小一定是2 的幕,即如果散列桶数组要扩展的话,也是以每次在原大小基础上乘以2
的形式扩展。 - unsigned int sizearray : 数组部分的大小。
- struct Table *metatable :存放该表的元表。
- TValue *array :指向数组部分的指针。
- Node *node :指向该表的散列桶数组起始位置的指针。
- Node *lastfree : 指向该表散列桶数组的最后位置的指针。
- GCObject *gclist : GC链表。
是lsizenode使用的是byte类型:由于在散列桶部分,每个散列值相同的数据都会以链表的形式串起来,所以即使数量用完了,也不要紧。因此这里使用byte 类型,而且是原数据以2为底的对数值,因为要根据这个值还原回原来的真实数据,也只是需要移位操作罢了,速度很快。
//lobject.h
typedef struct Node {
TValue i_val;
TKey i_key;
} Node;
typedef union TKey {
struct {
TValuefields;
int next; /* for chaining (offset for next node) */
} nk;
TValue tvk;
} TKey;
#define TValuefields Value value_; int tt_
typedef struct lua_TValue {
TValuefields;
} TValue;
一般情况下,如果看到一个数据类型是union ,就可以知道这个数据想以一种较为省内存的方式来表示多种用途,而这些用途之间是“互斥”的,也就是说,在某个时刻该数据类型只会是其中的一个含义。
操作算法
查找
- lobject.h的
findindex
函数
如果 输入的key是一个正整数,并且它的 值> 0 && <=数组大小
尝试在数量且部分查找
否则尝试在散列表部分查找
计算出该key 的散列值.根据此散列值访问Node 数数组得到散列桶所在的位置
遍历该做列树下的所有链农元素,直到找到该key 为止
新增元素
- lobject.h的
luaH_newkey
函数:根据Key,返回TValue指针。
/*
** inserts a new key into a hash table; first, check whether key’s main
** position is free. If not, check whether colliding node is in its main
** position or not: if it is not, move colliding node to an empty place and
** put new key in its main position; otherwise (colliding node is in its main
** position), new key goes to an empty position.
*/
(1)根据key来查找其所在散列桶的mainposition ,如果返回的结果中,该Node 的值为nil ,那么直接将key 赋值并且返回Node 的TValue指针就可以了。
(2)再则说明该mainposition 上已经有其他数据了,需要重新分配空间给这个新的key ,然后将这个新的Node 串联到对应的散列桶上。
- 散列表部分的数据组织是,首先计算数据的key 所在的桶数组位置,这个位置称为
mainposition 。相同mainposition 的数据以链表形式组织:
rehash:重新分配表空间
- lobject.h的
rehash
函数:
- 分配一个位图nums (
unsigned int nums[MAXABITS + 1];
),将其中的所有位置0 。这个位图的意义在于: nums 数组中第 i 个元素存放的是key在 2(i-1) 和 2i 之间的元素数量。 - 遍历Lua表中的数组部分,计算其中的元素数量,更新对应的nums 数组中的元素数量( numusearray 函数)。
- 遍历lua表中的散列桶部分,因为其中也可能存放了正整数,需要根据这里的正整数数量更新对应的nums数组元素数量(numusehash 函数)。
- 此时nums数组已经有了当前这个Table 中所有正整数的分配统计,逐个遍历nums 数组,获得其范围区间内所包含的整数数量大于50% 的最大索引,作为重新散列之后的数组大小,超过这个范围的正整数,就分配到散列桶部分了( computesizes 函数) 。
- 根据上面计算得到的调整后的数组和散列桶大小调整表( resize 函数)。
优化:避免重新散列操作
local a = {}
for i=1,3 do
a[i] = true
end
-- 最开始, Lua创建了一个空表a 。
-- 在第一次迭代中, a[l]为true 触发了一次重新散列操作, Lua将数组部分的长度设置为2^0 ,即l,散列表部分仍为空。
-- 在第二次迭代中, a[2]为true再次触发了重新散列操作,将数组部分长度设为2^1,即2。
-- 最后一次迭代又触发了一次重新散列操作,将数组部分长度设为2^2 ,即4。
local a = {true, true, true}
-- Lua知道表有3个元素,就直接创建了3个元素的数组。
迭代
- lobject.h的
luaH_next
函数:
在数组部分查找数据:
查找成功, 则返回该key 的下一个数据
否则 在散列桶部分查找数据:
查找成功, 则返回该key 的下一个数据
否则
返回0,表示没有元素了
取长度
- lobject.h的
luaH_getn
函数:取数组部分长度
/*
** Try to find a boundary in table 't'. A 'boundary' is an integer index
** such that t[i] is non-nil and t[i+1] is nil (and 0 if t[1] is nil).
*/
lua_Unsigned luaH_getn (Table *t) {
unsigned int j = t->sizearray;
if (j > 0 && ttisnil(&t->array[j - 1])) {
/* 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;
}
/* else must find a boundary in hash part */
else if (isdummy(t)) /* hash part is empty? hash部分为空的时候酶制剂返回sizearray*/
return j; /* that is easy... */
else return unbound_search(t, j);
}