一. 前言
前文中我们详细讲解了Lua数值结构的设计和实现思路,本文则完善配套于数值结构的各种操作处理函数。
二. 字符串操作
Lua的字符串设计为每次添加时先去总表取哈希看看是否有同样的哈希值,有同样的则不会新加,否则创建新字符串从而节约内存。因此主要操作就是新建以及新建附带的总表扩容,删除相对简单这里略过不提,因为每个不同的都会新建,所以不存在字符串的修改逻辑,从而提高了运算效率。
新建字符串时,会调用luaS_new()/luaS_newlstr()
函数,这里主要逻辑为
- 计算哈希值
- 根据哈希值去总表中找是否存在
- 如果存在则返回该对应哈希值的字符串
- 如果不存在则调用
newlstr()
新建
1 |
|
newlstr()
的新建逻辑比较简单,很常规的三步走
- 建立
TString
并赋值 - 加入对应的哈希表链中
- 检测哈希表是否需要扩容
1 | static TString* newlstr(lua_State* L, const char* str, size_t l, |
重新分配新的总字符串表的逻辑也很清晰
- 申请新空间并初始化
- 遍历旧表并挨个赋值给新表
- 释放旧表空间
1 | void luaS_resize(lua_State* L, int newsize) |
三. 表操作
表的操作相对来说会复杂一些,创建还好说,插入元素时,因为涉及了索引为数字的数组和键值型数组,所以需要考虑以下问题
- 插入数字索引的数组还是键值数组
- 插入键值数组的话,是否会导致单链过长复杂度退化以及空间浪费
- 扩容问题
首先看看最简单的创建表,其实就是申请内存然后赋初始值,这里一开始的时候,两个数组都是空的
1 | Table *luaH_new (lua_State *L, int narray, int nhash) { |
然后我们来考虑插入的事情,先回顾以下表的两个存储结构。对于array来说其实就是一系列的TValue,索引为数字。而Node则为k/v键值对,其中k考虑到哈希冲突的问题所以涉及了链表结构,对于同哈希的则存于链表之中。
1 | struct Table |
如果仅使用拉链法解决哈希冲突,当然是可行的,但是会带来一个问题:存在大量的空间浪费,因为必然有些哈希字段是未被模到的,所以如何合理使用这些没有哈希到的空桶就可以节约内存。其实节约空间的方法一般也很固定了:开放寻址法,也就是使用当前还空着的哈希槽,如果后续加入了该槽元素,再挪位即可。表中插入元素/替换元素的代码位置在luaV_settable()
,主要逻辑是
- 调用
luaH_set()
,根据key
获取一个table
的值指针oldval
- 将
val
的值写在在位置oldval
上
1 | void luaV_settable(lua_State* L, const TValue* t, TValue* key, StkId val) |
所以根据key
取找val
指针逻辑其实来自于luaH_set()
,该函数主要逻辑为
- 根据
key
调用luaH_get()
尝试取val
指针- 取到不为空则返回
- 为空则调用
newkey()
新建key
并返回val
指针
1 | TValue* luaH_set(lua_State* L, Table* t, const TValue* key) |
luaH_get()
实际会根据key的类型不同,调用luaH_getstr()
或者luaH_getnum()
或者直接default循环查找,其中luaH_getnum()
尝试在数组上寻找值并返回,如果找不到,则逻辑上和其他两个函数是统一的:
- 调用
mainposition()
取去哈希槽位置 - while循环去遍历链表找key值相同的位置输出
newkey()
会在表中创建key-val
位置,这里也是上文提到的拉链法+开放寻址法的关键实现位置
该函数代码逻辑主要为
- 调用
mainposition()
获取key对应的哈希槽位置mp
mp
位置如果有值,则存在两种可能:有相同哈希需要加入链表,或者开放寻址法被其他节点占用了,需要挪开- 先试图取一个空位置
n
(如果相同哈希的情况,则需要放key;如果是被占用了,需要把占用的节点挪开),取不到的话需要rehash()
扩容 - 看看
mp
节点的key
是否就是该哈希槽- 确实是该槽的节点,那就将n插在
mp
链表中,新的key要插入n位置 - 不是该槽的节点,则挪到n中,将
mp
空出来
- 确实是该槽的节点,那就将n插在
- 先试图取一个空位置
- 根据上面找到的位置,安放
key
,并返回其val
指针
1 | static TValue* newkey(lua_State* L, Table* t, const TValue* key) { |
至此,剩下的问题rehash()
了。重新分配大小的逻辑其实说起来很简单,先计算当前需要多少,然后按需分配。但是由于这里存在整数数组和k/v数组,所以如何取舍二者间的关系就是很重要的事了。对于Lua来说,有这么一个判定:希望在调整过后,数组在每一个2次方位置,其容纳的元素数量都超过了该范围的50%。不难想到想做到这个效果可以先按次方统计到一个数组,然后计算前缀和去比较即可。所以rehash()实现逻辑为
- 调用
numusearray()
统计整数数组部分 - 调用
numusehash()
统计k/v部分 - 调用
computesizes()
去计算分配多大的整数数组合适 - 调用
resize()
完成实际分配
1 | static void rehash(lua_State* L, Table* t, const TValue* ek) { |
每个函数的实现其实都相对简单,这里就不展开讨论了。
四. 总结
本篇文章详细介绍了数值结构体相关操作的实现和思路,希望能有所启发。
参考文献
《A No Frills Introduction to Lua 5.1 VM Instructions》
《The Implementation of Lua 5.0》