深度剖析Lua Table的运作方式

前言:本篇基于Lua-5.3.6源码并配合《Lua 解释器构建:从虚拟机到编译器》一书进行Table的运作解读。

一、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 */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;

如图:

CommonHeader:   GC的公共头部,标识是GC管理的对象,Table也是一个GC对象。

flags:标记缺失的元方法。比如:flags&(1<<TM_INDEX)为真表示缺少_index元方法。

lsizenode:哈希表大小的log2值,一来方便表示更大的存储容量,二来哈希表的扩容是成2倍增长的。

arraysize:数值的大小。

array:数组部分的指针。

node:哈希表部分的指针。

lastfree:哈希表空闲位置标记(指向最后一个空闲节点)

metatable:Table的元表,用来定义特定对象的元方法(如 __index、__newindex、__add 等)。

gclist:GC链表指针。

可以看到Table实际上是由两部分组成,数组部分哈希表部分。

二、键值的哈希计算

在完成了对key的哈希运算以后,就需要根据得到的哈希值,将其换算成表结构node数组的索 引值,计算的公式如下。

index=hash_value&(2^{lsizenode}-1)

这里-1是希望hash的低位全是1。

举例:假设有个字符串为"table",计算它在大小为8的哈希表的索引。

假设根据方法已经得到哈希值,01101011 00100100 10001101 00101100

lsizenode=\log_{2}8=3

那最终相与 只看后四位的结果  1100&0111=0100=4

key为“table"的node,将会被定位到hash[4]的位置上

三、Table查找元素

分两种情况,key值是int和非int

a.key是int

1)令被查找元素的key值为k,表array数组的⼤⼩为arraysize。

2)判断被查找元素的key值是否在数组范围内(即k≤arraysize是否成⽴)。

3)若key值在表的数组范围内,则返回array[k-1],流程终⽌。

4)若key值不在表的数组范围内,计算key值在哈希表中的位置,计算⽅式为

index=k&(2^{lsizenode}-1),然后以hash[index]节点为起点,查找key值与k相等的node,如果没找到则返回nil。

b.key是非int

1)计算被查询元素的key值(记为k)的哈希值,记为key_hash。

2)计算key值在哈希表中的位置,计算⽅式为index=k&(2^{lsizenode}-1),然后以 hash[index]节点为起点,查询key值与k相等的node,如果没找到则返回nil。

四、Table更新和插入

1)假设要新建的key值为k,计算k的哈希(hash)值,记为k_hash

2)计算key值在哈希表的索引,计算⽅式为index=k_hash &(2lsizenode-1)。

3)如果hash[index]的value值为nil,将其的key值设置为k的值,并返回value_对象指针,供调 ⽤者设置。

4)如果hash[index]的value值不为nil,需要分以下两种情况处理。

a. 计算node key的hash值,重新计算它的索引值。如果计算出来的索引位置不是hash[index], 那么lastfree不断左移,直⾄找到⼀个空闲的节点。将其移动到这⾥,修改链表关系,令其上⼀ 个与⾃⼰索引值相同节点的next值指向⾃⼰(如果存在的话)。新插⼊的key和value设置到 hash[index]节点上。

b. 计算node key的hash值,重新计算它的索引值,如果计算出来的索引位置就是hash[index], 那么lastfree不断左移,直⾄找到⼀个空闲的节点。将新插⼊的key和value值设置到这个节点 上,并调整链表关系,将hash[index]的key值的next值指向新插⼊的节点。

举例:

向表插⼊⼀个 key值为5、value值为“xixi”的元素。

由于key值5超出了数组的⼤⼩范围,那么程序⾸先会尝试 去哈希表中查找,可以得到最终index的值为1。hash[1]的key值为nil,与要更新元素的key值不 相等,于是触发了插⼊操作。由于hash[1]的key和value值均是nil,因此可以将该元素直接设置 到这⾥。

向表插⼊⼀个 key值为13、value值为“manistein”的元素。

根据公式算出index为1,因为hash[1]的value 域的值为“xixi”、key值为5,与k值13并不相等,于是便发⽣了哈希碰撞。key值5经过转换运算 得到的哈希表index的值为1,此时它就在这个位置上,因此key值为13的新元素需要被移⾛。 lastfree指针向左移动,并且将key值为13、value值为“manistein”的元素赋值到lastfree指向的位置上(即hash[3]的位置上),并且将hash[1]的key的next指向lastfree指针所指的位置。

注意:这里的next值指的是与当前index相隔的距离,而不是下一个节点的index值,因为这里的哈希表本身是个链表,存index值没有意义

向表插⼊⼀个 key值为7、value值为“wu”的元素。

经过计算得到其对应的hash表 index值为3,此时hash[3]已经被占⽤。此时需要计算占据在这⾥的元素的key值,其真实对应 的hash表index其实是1,因为hash[1]被占⽤才被移动到这⾥。因为这个元素计算得到的index 与当前位置并不匹配,因此lastfree指针需要继续向左移动,并将key值为13的元素迁移到这 ⾥,并更新其前置节点的next域。最后将key值为7的元素,赋值到hash[3]的位置上.

五、Table扩容机制

更新和插⼊的操作,均是在哈希表空间充⾜的情况下进⾏的,当哈希表 已满,且⼜有新的元素要插⼊哈希表时,将触发表的resize操作。首先调整的是数组的大小

1)统计要调整⼤⼩的Lua表、数组和哈希表中的有效元素(值类型不为nil的元素)的总数

2)创建⼀个int类型的数组,它的⼤⼩为32,将其命名为nums。nums [i]表示的信息量⽐较 ⼤。⾸先i表示⼀个数值区间,这个区间是(2^{i-1}2^{i}]

3)统计数组的分布情况,假设arraysize是66,且每个Value都不为空,那么此时它的分布情况是

66=2^{0}+2^{1}+2^{2}+2^{3}+2^{4}+2^{5}+3

4)统计哈希表元素在nums [32]中不同区间的分布情况,伪代码如下

//lsizenode是Table数据结构中衡量哈希表长度的变量
for(int i=0;i<pow(2,lsizenode);i++){if(hash[i].key!=null&&isInt(hash[i].key)){int k=ceillog2(hash[i].key);nums[k]++;}
}//找到hash key值在Nums数组中的下标
void ceillog2(int hashKey){for(int i=0;i<32;i++){if(pow(2,i)>=hash.key){return i;}}
}

5)判断新插⼊元素new_element的key值是否为整类型,如果是则令

nums [FindIndex (new_element.key)]++。

6)完成数组nums的统计之后,根据nums计算新的数组⼤⼩。在数组⼤⼩范围内,值不为nil的 元素要超过数组⼤⼩的⼀半,其计算公式如下。

int asize=0;
for(int i=0;i<32;i++){asize+=nums[i];if(asize>pow(2,i)/2){sizearray=pow(2,i); //sizearray是Table中衡量数组大小的变量}
}

7)计算在数组⼤⼩范围内有效元素的个数,记为array_used_num。

8)当数组⼤⼩⽐原来⼤时,扩展原来的数组到新的⼤⼩,并将哈希表中key值≤arraysize,且 >0的元素转移到数组中,并将哈希表⼤⼩调整为ceillog2(total_element-array_used_num), 同时对每个node进⾏重新定位位置。

数组扩大的简单理解:数组部分加入哈希表里面可以转移到数组里的键值对,此时数组部分超过一半都是不为nil。

示例:

9)当数组⼤⼩⽐原来⼩时,缩⼩原来的数组到新的⼤⼩,并将数组中key值超过数组⼤⼩的元 素转移到哈希表中。此时哈希表⼤⼩调整为ceillog2(total_element-array_used_num),同时 对每个node进⾏重新定位位置。

数组缩小的简单理解:数组不连续的key转移到哈希表,此时数组超过一半都是nil。

基于8-9至此我们可以推理到一个二级结论:

哈希表里最新的int的类型key值一定大于数组长度(sizearray)。

这个结论对于后续Table的遍历起到了一定的理论依据。

六、Table遍历

Lua提供了luaH_next函数来进⾏迭代操作,函数申明如下

方法中关键调用是findIndex方法,5.3源码如下

/*
** returns the index of a 'key' for table traversals. First goes all
** elements in the array part, then elements in the hash part. The
** beginning of a traversal is signaled by 0.
*/
static unsigned int findindex (lua_State *L, Table *t, StkId key) {unsigned int i;if (ttisnil(key)) return 0;  /* first iteration */i = arrayindex(key);if (i != 0 && i <= t->sizearray)  /* is 'key' inside array part? */return i;  /* yes; that's the index */else {int nx;Node *n = mainposition(t, key);for (;;) {  /* check whether 'key' is somewhere in the chain *//* key may be dead already, but it is ok to use it in 'next' */if (luaV_rawequalobj(gkey(n), key) ||(ttisdeadkey(gkey(n)) && iscollectable(key) &&deadvalue(gkey(n)) == gcvalue(key))) {i = cast_int(n - gnode(t, 0));  /* key index in hash table *//* hash elements are numbered after array ones */return (i + 1) + t->sizearray;}nx = gnext(n);if (nx == 0)luaG_runerror(L, "invalid key to 'next'");  /* key not found */else n += nx;}}
}

细分key值四种情况:

情况一:key=nil

返回数组的第一个元素。

情况二:key=整型&&key<sizearray

返回数组的下一个元素。

情况三:key=整型&&key==sizearray

返回哈希表的第一个元素。

情况四:key!=整型

返回哈希表的下一个元素。

七、解读和Table相关的接口

1)pairs和ipairs

  • pairs(t):用于遍历表中所有键值对(无序),默认使用内建的 next 函数遍历所有键。顺序不保证,对稀疏表或散列部分都能遍历到。
  • ipairs(t):用于按整数索引从 1 开始顺序遍历,直到遇到第一个 nil 为止。常用于“数组风格”的连续整数索引。

注意:ipairs并不仅仅遍历array部分,hash部分也会遍历,因为由前文可得,hash部分也会存在连续的整型key。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/94397.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/94397.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

PETR/PETRv2

PE: position embedding 一、PETR算法动机回归 1.1 DETR 输入组成&#xff1a;包含2D位置编码和Object Query 核心流程&#xff1a;通过Object Query直接索引2D特征图&#xff0c;结合位置编码迭代更新Query 特点&#xff1a;整体流程简洁&#xff0c;每个Query代表一个潜在目标…

计算机大数据毕业设计推荐:基于Spark的气候疾病传播可视化分析系统【Hadoop、python、spark】

精彩专栏推荐订阅&#xff1a;在下方主页&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; &#x1f496;&#x1f525;作者主页&#xff1a;计算机毕设木哥&#x1f525; &#x1f496; 文章目录 一、项目介绍二、…

英伟达显卡GPU驱动的本质

我们来深入、详细地探讨一下英伟达&#xff08;NVIDIA&#xff09;GPU驱动程序的本质。 普通用户眼中的驱动程序可能只是一个“让显卡工作的软件”&#xff0c;但它的本质远比这复杂和深刻。我们可以从几个层面来理解它。 核心比喻&#xff1a;翻译官、指挥官与优化大师 如果说…

算法 ---哈希表

一、哈希介绍 是什么 存储数据的容器 什么用 快速查找某个元素 什么时候用哈希表 频繁的查找某一个数的时候 怎么用哈希表 &#xff08;1&#xff09;容器&#xff08;哈希表&#xff09; &#xff08;2&#xff09;用数组模拟哈希表&#xff08;字符串的字符&#xf…

基于分布式环境的令牌桶与漏桶限流算法对比与实践指南

基于分布式环境的令牌桶与漏桶限流算法对比与实践指南 在高并发的分布式系统中&#xff0c;限流是保障服务可用性和稳定性的核心手段。本文聚焦于令牌桶算法与漏桶算法在分布式环境下的实现与优化&#xff0c;对多种解决方案进行横向对比&#xff0c;分析各自的优缺点&#xff…

WPF MVVM入门系列教程(TabControl绑定到列表并单独指定每一页内容)

在以前的开发过程中&#xff0c;对于TabControl控件&#xff0c;我一般是习惯直接定义TabItem&#xff0c;在TabItem下布局&#xff0c;并进行绑定。 类似这样 1 <TabControl ItemsSource"{Binding TabList}" SelectedIndex"0">2 <TabItem…

L2CAP 面向连接信道(CoC)在 BLE 中的应用:建立、流控与数据传输

在物联网(IoT)蓬勃发展的今天,低功耗蓝牙(BLE)技术因其高效节能、低成本等特性,成为短距离无线通信的首选方案。作为 BLE 协议栈的核心组件,逻辑链路控制与适配协议(L2CAP)的面向连接信道(CoC)承担着数据传输的关键任务。本文将深入解析 L2CAP CoC 在 BLE 中的应用,…

医疗AI与医院数据仓库的智能化升级:异构采集、精准评估与高效交互的融合方向(上)

摘要: 随着医疗信息化建设的深入,医院数据仓库(Data Warehouse, DW)作为医疗AI应用的核心数据底座,其效能直接决定智能化转型的深度与广度。本文聚焦医疗AI驱动下医院数据仓库的三大关键升级功能——异构采集支持数据库体检与智能SQL分析、评估引擎重构实现六大数据库精准…

2015-2018年咸海流域1km归一化植被指数8天合成数据集

数据集摘要数据集包含2015年-2018年咸海流域NDVI 8天均值数据。提取美国国家航空航天局中分辨率成像光谱仪MOD13A2产品第一波段作为归一化植被指数数据&#xff0c;乘以比例因子0.0001&#xff0c;叠加咸海流域边界数据&#xff0c;裁切后得到咸海流域范围内的NDVI月均值数据。…

Kafka消息持久化机制全解析:存储原理与实战场景

目录 引言​ 一、Kafka消息持久化的核心目标 二、底层存储机制深度剖析 1.【文件系统分层】——日志分组 日志段 核心结构 示例目录结构 2.【消息写入流程】——从内存到磁盘的旅程✈️ 3.【默认存储参数】——生产环境的黄金比例 三、典型应用场景与案例实战 案例1…

Python训练营打卡Day41-Grad-CAM与Hook函数

知识点回顾回调函数lambda函数hook函数的模块钩子和张量钩子Grad-CAM的示例 作业&#xff1a;理解下今天的代码即可 在深度学习中&#xff0c;我们经常需要查看或修改模型中间层的输出或梯度。然而&#xff0c;标准的前向传播和反向传播过程通常是一个黑盒&#xff0c;我们很难…

使用VBA宏批量修改Word中表格题注格式

目录&#x1f4c2; 使用步骤✅ 方式一&#xff1a;应用已有样式&#xff08;推荐&#xff09;代码实现说明✅ 方式二&#xff1a;手动设置字体格式&#xff08;无需预定义样式&#xff09;代码实现参数说明如何运行宏&#xff1f;补充建议总结在撰写论文、技术文档或报告时&…

Redis面试精讲 Day 27:Redis 7.0/8.0新特性深度解析

【Redis面试精讲 Day 27】Redis 7.0/8.0新特性深度解析 在“Redis面试精讲”系列的第27天&#xff0c;我们将聚焦Redis最新版本——7.0与8.0的核心新特性。随着Redis从内存数据库向云原生、高可用、高性能中间件持续演进&#xff0c;7.0和8.0版本引入了多项颠覆性改进&#xf…

使用自制的NTC测量模块测试Plecs的热仿真效果

之前构建的 NTC 温度测量模型是进行 PLECS 热仿真的完美起点和核心组成部分。 PLECS 的强大之处在于它能够进行多域仿真,特别是电-热联合仿真。您可以将电路仿真(包括您的 NTC 测量模型)与热仿真(散热器、热容、热阻等)无缝地结合起来。 电-热联合仿真原理 整个仿真闭环…

C语言初学者笔记【动态内存管理】

、 文章目录一、为什么需要动态内存分配&#xff1f;二、malloc 和 free1. malloc2. free三、calloc 和 realloc1. calloc2. realloc四、常见的动态内存错误1. 对 NULL 解引用2. 越界访问3. 对非动态内存使用 free4. 释放部分动态内存5. 多次释放同一块内存6. 内存泄漏五、动态…

AI模型部署 - 大语言模型(LLM)部署技术与框架

目录 一、 大语言模型部署的核心挑战与关键技术 二、 主流开源部署框架深度解析 2.1. Ollama:本地部署的极简主义者 2.2. Hugging Face TGI (Text Generation Inference) 2.3. vLLM:为吞吐量而生 2.4. sglang:面向复杂提示与结构化输出的革新者 三、 特定硬件与云平台…

Windows11 GeForce GTX 1060 CUDA+CUDNN+Pytorch 下载及安装

一、查看显卡型号信息 系统&#xff1a;Windows11 显卡&#xff1a;GeForce GTX 1060 型号&#xff1a; &#xff08;1&#xff09;搜索 NVIDIA&#xff0c;选择 NVIDIA Control Panel&#xff08;2&#xff09;打开 NVIDIA control Panel&#xff0c;打开系统信息&#xff0c;…

在通义灵码中配置MCP服务

目录 查找mcp列表 通义灵码中配置MCP 使用方式 STDIO (Standard Input/Output) 组成部分&#xff1a; SSE (Server-Sent Events) 特点&#xff1a; 主要区别对比 配置方式 配置优先级 个人设置 项目设置 验证 通过MCP调用高德地图 查找mcp列表 打开ModelScope - …

网络中的IO问题(五种常见的IO方式)

什么是高效的IO&#xff1f; 正常情况下&#xff0c;IO等拷贝 高效的IO拷贝&#xff08;即让IO尽量不等&#xff09; 为什么我们平常玩电脑的时候&#xff0c;感觉不到等待的过程呢&#xff1f; 任何通信场景&#xff0c;IO通信场景&#xff0c;效率一定是有上限的. 花盆里&am…

JAVA核心基础篇-修饰符

Java 修饰符主要用于定义类、方法或变量&#xff0c;通常放在语句的最前端&#xff0c;可分为访问修饰符和非访问修饰符两类。一、访问修饰符public&#xff1a;对所有类可见&#xff0c;可用于类、接口、变量和方法。被声明为 public 的类、方法、构造方法和接口能够被任何其他…