添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

【Lua源码赏析】第四章 Table 的实现

前言

table是Lua统一的数据结构,所有的数据结构都可以用table实现。
table是开发中最常用的一个lua语法,所以我最先开始看了table的实现

4.1 数据结构

首先我们了解一下table的数据结构
typedef union TKey {
  struct {
    TValuefields;
    struct Node *next;  /* for chaining */
  } nk;
  TValue tvk;
} TKey;
typedef struct Node {
  TValue i_val;
  TKey i_key;
} Node;
// lua table的基本数据结构
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;     /* hash part */
  Node *lastfree;  /* any free position is before this position */
  GCObject *gclist;
  int sizearray;  /* size of `array' array */
} Table;

为了效率,Lua 的官方实现,又把 table 的储存分为 数组部分 哈希表部分

  • 数组部分 ,从 1 开始作整数数字索引。这可以提供紧凑且高效的随机访问。
数组部分存储在TValue *array 中,其长度信息存于int sizearray
  • 哈希表部分 ,唯一不能做哈希键值的是 nil ,这个限制可以帮助我们发现许多运行期错误。
哈希表存储在Node *node,哈希表的大小用lu_byte lsizenode 表示。lsizenode表示的是2的几次幂,而不是实际大小,因为哈希表的大小一定是2的整数次幂

每个 Table 结构,最多会由三块连续内存构成。

  • 一个 table 结构,
  • 一块存放了连续整数索引的 数组
  • 和一块大小为 2 的整数次幂的 哈希表

小优化 :哈希表的最小尺寸为 2 的 0 次幂,也就是 1 。为了减少空表的维护成本,Lua在这里做了一点优化。

它定义了一个不可改写的空哈希表:dummynode 。让空表被初始化时,node 域指向这个 dummy 节点。它虽然是一个全局变量,但因为对其访问是只读的,所以不会引起线程安全问题。
#define dummynode		(&dummynode_)
static const Node dummynode_ = {
  {NILCONSTANT},  /* value */
  {{NILCONSTANT, NULL}}  /* key */
};

4.1.1 创建table

Table *luaH_new (lua_State *L) {
  Table *t = &luaC_newobj(L, LUA_TTABLE, sizeof(Table), NULL, 0)->h;
  t->metatable = NULL;
  t->flags = cast_byte(~0);
  t->array = NULL;
  t->sizearray = 0;
  setnodevector(L, t, 0);
  return t;
}

其中 setnodevector用来初始化哈希表部分。内存管理部分则使用了luaM 相关 API 。

4.1.2 销毁table

void luaH_free (lua_State *L, Table *t) {
  if (!isdummy(t->node))
    luaM_freearray(L, t->node, cast(size_t, sizenode(t)));
  luaM_freearray(




    
L, t->array, t->sizearray);
  luaM_free(L, t);
}

4.2 算法

Table 按照 lua 语言的定义,需要实现四种基本操作:读、写、迭代和获取长度。lua 中并没有删除操作,而仅仅是把对应键位的值设置为 nil 。

4.2.1 插入

/*
** 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.
向哈希表插入一个新键:  首先检查一下新键的 mainposition 是否存在?
   若无人使用,则可以直接设置为这个新键。
   若之前有其它键占据了这个位置,则检查占据此位置的键的mainposition 是不是这里?     
      若两者位置冲突,则利用Node结构的next域,以一个单向链表的形式把它们链起来;     
      否则,新键占据这个位置,而老键更换到新位置并根据它的主键找到属于它的链的
            那条单向链表中上一个结点,重新链入。
TValue* luaH_newkey(lua_State* L, Table* t, const TValue* key) {
	Node* mp;
	if (ttisnil(key)) 
		luaG_runerror(L, "table index is nil");
	else if (ttisnumber(key) && luai_numisnan(L, nvalue(key)))
		luaG_runerror(L, "table index is NaN");
	mp = mainposition(t, key);
	if (!ttisnil(gval(mp)) || isdummy(mp))
	{  /* main position被占用 */
		Node* othern;
		Node* n = getfreepos(t);  /* 获取一个空闲的位置 */
		if (n == NULL) {  /* 没有空闲位置了 */
			rehash(L, t, key);  /* 扩容 */
			/* 调用'newkey'的时候注意缓存和GC边界*/
			return luaH_set(L, t, key);  /* 设置值 */
		lua_assert(!isdummy(n));
		othern = mainposition(t, gkey(mp));
		if (othern != mp) {
			/* 冲突节点不是在它自己的main position上 */
			while (gnext(othern) != mp) 
				othern = gnext(othern);  /* 找到main position的前一个节点 */
			//移动冲突节点到空闲位置
			gnext(othern) = n;  /* redo the chain with `n' in place of `mp' */
			*n = *mp;  /* 复制冲突节点到一个空闲位置 (mp->next also goes) */
			gnext(mp) = NULL;  /* now `mp' is free */
			setnilvalue(gval(mp));
		else {
			/* 冲突节点在它自己的main position上 */
			//移动新节点到一个空闲节点
			gnext(n) = gnext(mp);  /* chain new position */
			gnext(mp) = n;
			mp = n;
	setobj2t(L, gkey(mp), key);
	luaC_barrierback(L, obj2gco(t), key);
	lua_assert(ttisnil(gval(mp)));
	return gval(mp);
}

创建key的流程如下所示:

  • 计要新建的key值为k,并计算k的hash值,记为k_hash
  • 计算key应该落在hash表的哪个位置,计算方式为index = k_hash & (2^lsizenode-1)
  • 如果hash[index]这个node的value值为nil,将node的key值设置为k的值,并返回value_对象指针,供调用者设置
  • 如果hash[index]这个node的value值不为nil,需要分两种情况处理
    • 计算node key的hash值,如果经过定位运算后,index的值不在自己所处的位置上,那么lastfree不断左移,直至找到一个空闲的节点,将其移动到这里,修改链表关系,令其上一个与自己计算得到相同index值的节点的next域指向自己(如果存在的话)。新插入的key和value设置到hash[index]节点上
    • 计算node key的hash值,如果经过定位运算后,index的值在自己所处的位置上,那么lastfree不断左移,直至找到一个空闲的节点,将自己的key和value值设置到这个节点上,并调整链表关系,将与自己计算得到相同index值的上一个节点的key的next指向自己的位置。

接下来我们举个栗子,来理解一下插入操作

假设一个array的size为4,hash表的size为4的table,所有域的值都是nil,如图10所示,现在要向table塞入一个key值为5,value值为”xixi”的元素,由于key值5超出了array的size范围,那么程序首先会尝试去hash表中查找,我们可以得到最终index的值为1,由于hash[1]这个Node的key值为nil,与要更新元素的key值不相等,因此此时触发了插入操作,由于hash[1]这个Node的key和value均是nil,因此可以将该元素直接设置到这里。

图10

与此同时,一个key值为13,value值为”manistein”的元素也要对table进行赋值,经过之前阐述过的方式计算,得到index值为1,在这种情况下直接在hash表中进行查找,因为hash[1]的value域的值为”xixi”并不是nil,key值为5,与13并不相等,于是此时 发生了hash碰撞 ,key值5经过转换运算,得到的hash表index的值为1,此时他就在这个位置上,因此 key值为13的新元素需要被移走 ,lastfree指针,此时向左移动,并且将key值为13,value值为”manistein”的元素,赋值到lastfree指向的位置上(即hash[3]的位置上),并且将hash[1]的key的next指向lastfree指针所指的位置,如图11所示:

图11

又再次,一个key值为7,value值为”wu”的元素要对table进行赋值,经过计算得到其对应的hash表index值为3,此时hash[3]已经被占用, 此时需要计算,占据在这里的元素的key值,其真实对应的hash表index其实是1,因为hash[1]被占用才被移动到这里,因为这个元素计算得到的index与当前位置并不匹配 ,因此lastfree指针需要继续向左移动,并将key值为13的元素迁移到这里,并更新其前置节点的next域,最后将key值为7的元素,赋值到hash[3]的位置上,如图12所示:

图12

至此,我们完成了整个插入流程。

4.2.2 查询部分

/*
** main search function
const TValue* luaH_get(Table* t, const TValue* key) {
	switch (ttype(key)) {
	case LUA_TNIL: return luaO_nilobject;
	case LUA_TSHRSTR: 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_getint(t, k);  /* use specialized version */
		  /* else go through */
	default: {
		Node* n = mainposition(t, key);
		do {  /* check whether `key' is somewhere in the chain */
			if (luaV_rawequalobj(gkey(n), key))




    

				return gval(n);  /* that's it */
			else n = gnext(n);
		} while (n);
		return luaO_nilobject;
}

当查询键为整数键且在数组范围内时,在数组部分查询;

否则,根据键的哈希值去哈希表中查询。拥有相同哈希值的冲突键值对,在哈希表中由 Node 的 next 域单向链起来,所以遍历这个链表就可以了。

4.2.3 扩容部分:rehash

static void rehash(lua_State* L, Table* t, const TValue* ek) {
	int nasize, na;
	//创建一个nums[32]的数组,nums[i]表示key值为int型,且key值在(2i-1, 2i]范围内
	//(lua脚本里table的int型下标,如t[1]的key在(2-1, 20]这个区间),并且value不为nil的元素个数
	int nums[MAXBITS + 1];  /* nums[i] = number of keys with 2^(i-1) < k <= 2^i */
	int i;
	int totaluse;
	for (i = 0; i <= MAXBITS; i++) 
		nums[i] = 0;  /* 初始化值 */
	nasize = numusearray(t, nums);  /* 统计array在nums数组中,不同区间的分布情况 */
	totaluse = nasize;  /* 所以int型key数量*/
	totaluse += numusehash(t, nums, &nasize);  /* count keys in hash part */
	/* count extra key */
	nasize += countint(ek, nums);
	totaluse++;//因为新插入一个元素,因此total_element += 1
	/* 计算数组部分的最新尺寸 */
	na = computesizes(nums, &nasize);
	/* 真正的扩容操作 */
	luaH_resize(L, t, nasize, totaluse - na);
void luaH_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 ... */
	if (nasize > oldasize)  /* 数组部分需要变长 */
		setarrayvector(L, t, nasize);
	/* 创建一个合适长度的哈希部分 */
	setnodevector(L, t, nhsize);
	if (nasize < oldasize) {  
		/* 数组部分需要缩短 */
		t->sizearray = nasize;
		/* re-insert elements from vanishing slice */
		for (i = nasize; i < oldasize; i++) {
			if (!ttisnil(&t->array[i]))
				luaH_setint(L, t, i + 1, &t->array[i]);
		/* 缩短数组 */
		luaM_reallocvector(L, t->array, oldasize, nasize, TValue);
	/* 把所有元素重新插入哈希部分*/
	for (i = twoto(oldhsize) - 1; i >= 0; i--) {
		Node* old = nold + i;
		if (!ttisnil(gval(old))) {
			/* doesn't need barrier/invalidate cache, as entry was
			   already present in the table */
			setobjt2t(L, luaH_set(L, t, gkey(old)), gval(old));
	if (!isdummy(nold))
		luaM_freearray(L, nold, cast(size_t, twoto(oldhsize))); /* 释放旧的数组 */
}

rehash 的主要工作是统计当前 table中到底有多少有效键值对,以及决定数组部分需要开辟多少空间。其原则是 最终数组部分的利用率需要超过 50%

lua 使用一个 rehash函数中定义在栈上的 nums 数组来做这个整数键统计工作。这个数组按 2 的整数幂次来分开统计各个区段间的整数键个数。统计过程的实现见 numusearray 和 numusehash 函数。

最终,computesizes 函数计算出不低于 50% 利用率下,数组该维持多少空间。同时,还可以得到有多少有效键将被储存在哈希表里。

根据这些统计数据,rehash 函数调用 luaH_resize 这个 api 来重新调整数组部分和哈希部分的大小,并把不能放在数组里的键值对重新塞入哈希表。

4.2.4 表的迭代部分

遍历table主要是ipairs和pairs两个函数。这两个函数都会在vm内部临时创建出两个变量state和index,用于对lua表进行迭代访问,每次访问的时候,会调用luaH_next函数

int luaH_next(lua_State* L, Table* t, StkId key) {
	int i = findindex(L, t, key);  /* find original element */
	for (i++; i < t->sizearray; i++) {  /* 先遍历数组部分 */
		if (!ttisnil(&t->array[i])) {  /* a non-nil value? */
			setnvalue(key, cast_num(i + 1));
			setobj2s(L, key + 1, &t->array[i]);
			return 1;
	for (i -= t->sizearray; i < sizenode(t); i++) {  /* 再遍历哈希部分 */
		if (!ttisnil(gval(gnode(t, i)))) {  /* a non-nil value? */
			setobj2s(L, key, gkey(gnode(t, i)));
			setobj2s(L, key + 1, gval(gnode(t, i)));
			return 1;
	return 0;  /* no more elements */
}

在大多数其它语言中,遍历一个无序集合的过程中,通常不允许对这个集合做任何修改。即使允许,也可能产生未定义的结果。在 lua 中也一样,遍历一个 table 的过程中,向这个 table 插入一个新键这个行为,将无法预测后续的遍历行为。 但是,lua 却允许在遍历过程中,修改 table 中已存在的键对应的值 。由于 lua 没有显式的从 table 中删除键的操作,只能对不需要的键设为空。

4.2.5 获取长度

lua 的 table 的长度定义只对序列表有效

它使用二分法,来快速在哈希表中快速定位一个非空的整数键的位置。

/*
** 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).
int 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;