直接读取 value table_【Lua源码赏析】第四章 Table 的实现

前言

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

4.1 数据结构

首先我们了解一下table的数据结构
typedef 

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

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

a520eb37ec56ddfdf0fa9d2f878c3aa3.png

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

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

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

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

4.1.1 创建table

Table 

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

4.1.2 销毁table

void 

4.2 算法

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

4.2.1 插入

/*

创建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,因此可以将该元素直接设置到这里。

9576e5d59bbd118b198813d54348ecd5.png
图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所示:

df195bb01f73d2fbcf762b71b1960cd9.png
图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所示:

c63ca49eec45dc3e825e31400f870d40.png
图12

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

4.2.2 查询部分

/*

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

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

4.2.3 扩容部分:rehash

static 

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 

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

4.2.5 获取长度

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

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

/*

4.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值,否则很容易出错。

参考

Lua表 源码解析

构建Lua解释器Part4:Table设计与实现

Lua中table类型源码分析_不很正派的专栏-CSDN博客

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值