lua数据结构原理,lua基础知识学习二

1.什么是lua数据结构原理

Lua是一门用C语言编写的脚本语言,一共1w多行代码,非常的轻巧,适合做游戏脚本。

Lua是解释型语言,通过对Lua的语言进行语法解析,然后生成二进制字节码,然后转由C语言进行执行操作。编译型语言,则会进行编译后生成机器码,直接由机器进行执行即可,执行效率会比较高。

所以,lua数据结构原理,其实就是用c写的lua的源码。

2.lua数据结构原理大体架构

3.如何看lua数据结构原理,lua数据结构原理的不同组件比较

3.1 全局状态机

从lua源码中的main函数看整个状态机的初始化:

Lua的main函数方法中,lua_State *L = luaL_newstate(); 主要用于创建全局状态机。

luaL_newstate主要用来为每一个LUA线程创建独立的函数栈和线程栈,以及线程执行过程中需要用到的内存管理、字符串管理、gc等信息。

全局状态机的初始化、销毁的实现,主要在lstate.c文件中。

int main (int argc, char **argv) {
  int status, result;
  /* 第一步:创建一个主线程栈数据结构 */
  lua_State *L = luaL_newstate();  /* create state */
  if (L == NULL) {
    l_message(argv[0], "cannot create state: not enough memory");
    return EXIT_FAILURE;
  }
  lua_pushcfunction(L, &pmain);  /* to call 'pmain' in protected mode */
  lua_pushinteger(L, argc);  /* 1st argument */
  lua_pushlightuserdata(L, argv); /* 2nd argument */
  status = lua_pcall(L, 2, 1, 0);  /* do the call */
  result = lua_toboolean(L, -1);  /* get result */
  report(L, status);
  lua_close(L);
  return (result && status == LUA_OK) ? EXIT_SUCCESS : EXIT_FAILURE;
}

数据结构lua_Stateglobal_State

Lua使用LG结构(lua_State *L)(LG结构就是lua_State和global_State结构)贯穿整个线程解析的生命周期的始终。

lua_newstate函数主要用于创建一个lua_State和global_State的数据结构。

lua_State:主线程栈结构。数据栈和调用栈。以及 lua 虚拟机中的环境表、注册表、运行堆栈、虚拟机的上下文等数据。
global_State:全局状态机,维护全局字符串表、内存管理函数、gc等信息。


lua_State和global_State结构,通过LG(指针引用)的方式进行链接,将全局状态机global_State挂载到lua_State数据结构上
global_State结构是完全感知不到的:我们无法用Lua公开的API获取到它的指针、句柄或引用
lua_State和global_State之间通过 global_State *l_G 互相建立链接关系。
LUA语言没有实现独立的线程,但是实现了协程序,关于协程后续会单独开一章节讲解

 3.2 数据栈:

主要由一个StkId结构的数组组成。StkId结构为TValue,支持字符串、函数、数字等数据结构。
所有的数据都通过lapi.c文件中的lua_push*函数向栈上压入不同类型的值(数字、字符串、函数等)。
L->stack:指向栈底部。每次将一个数据压入栈上,栈指针(L->top++)都会指向到下一个结构上。
L->stacksize:栈体的大小。在栈结构初始化的时候,会分配一个BASIC_STACK_SIZE=40大小的栈结构。(LUAI_MAXSTACK 32位:15000;64位:1000000)
L->stack_last:指向栈头部,但是会留空EXTRA_STACK=5个BUF,用于元表调用或错误处理的栈操作。
L->top:栈顶指针。压入数据,都通过移动栈顶指针来实现。

3.3 调用栈:

主要由一个CallInfo的结构组成。CallInfo是一个双向链表结构。通过双向链表结构来管理每一个Lua的函数调用栈信息。
Lua一共有三种类型的函数:C语言闭包函数(例如pmain)、Lua的C函数库(例如str字符串函数)和LUA语言
每一个函数的调用,都会新生产一个CallInfo的调用栈结构,用于管理函数调用的栈指针信息。当一个函数调用结束后,会返回CallInfo链表的前一个调用栈,直到所有的调用栈结束回到L->base_ci。
调用栈最终都会指向数据栈上,通过一个个调用栈,用于管理不同的函数调用。
每次调用栈调用函数完成后,都会将函数返回的结果在栈上移动位置,将结果集逐个从ci->func位置开始填充,并调整L->top栈顶指针。这样的好处就是调用完一个函数,数据栈上只需要存储最终函数返回的结果集,不会因为复杂的函数嵌套而导致整个栈体结构迅速扩大。
 

3.4.字符串

字符串操作对应的文件:lstring.c

字符串 - 数据结构

Lua的字符串管理都会统一挂载到global_State全局状态机上

字符串都会存储在TString对象结构上。Lua的字符串管理分两种类型:链表方式存储(短字符串(<40)) 和 HashMap 缓存方式

链表方式:存储在stringtable strt 的 链表结构上。
缓存方式:存储在TString *strcache[STRCACHE_N][STRCACHE_M] HashMap的结构上。

TString存储结构图

       Lua字符串的数据内容部分并未分配独立的内存来存储,而是直接追加在TString结构的后面。TString存储结构如下图:

       

         Lua字符串对象 = TString结构 + 实际字符串数据
        • TString结构 = GCObject *指针 + 字符串信息数据

3.5 table结构

在这里插入图片描述

结合了hash中的开放定址法和链地址法 

4.lua数据结构源码实现

4.1 lua中string的源码分析

从虚拟机的大体来看,字符串通过一个结构体存放global_State里,这个结构stringtable(lstate.h)是:

GCObject(lstate.h)的结构是:

stringtable结构体的字段含义是:

GCObject **hash: 一个GCObject的二维指针,可以看作二维数组,根据字符串的离散值存放这我们创建的字符串。通过Hash值可以指向Hash值存放的GCObject,其中实际引用的是TString。

lu_int32 nuse: 已经创建的TString个数

int size:TString的总个数(初始值为32)

在global_State中,会有一个stringtable,通过它来访问虚拟机中的所有字符串(包括长短字符串)

总结:

(1)Lua中通过字符串的hash值来对字符串进行查找,对比。
(2)Lua中的字符串变量存放的是字符串的引用,而不是字符串本身。
(3)同一个字符串在Lua虚拟机中只有一份。
(4)Lua虚拟机中global_State的strt字段存放着当前系统中的所有字符串

具体看看字符串的实现,主要数据结构体是TString(lobject.h)

TString关联的宏定义, L_Umaxalign(llimits.h)

TString关联的宏定义, CommonHeader(lobject.h),这一个定义很多地方会使用到

这是TString相关的结构体。

下面来是分析如何创建一个字符串,实现的文件是在lstring.c。

在lua里面,创建一个字符串,会根据字符串的长短来区分到底创建长还是短的字符串。

如果长度小于LUAI_MAXSHORTLEN(值为40),它就会创建一个短字符串;

否则的话就会创建长字符串。

短字符串的区别是:如果在创建的过程,发现hash值、长度和内容都一样的短字符串,就会复用它,不再去进行分配。

但是长的字符串就会不管三七二一都会创建一个新的TString.

在创建字符串之前,stringtable的长度会动态扩展,当字符串table分配的size都被用完了,并且当size不大于最大数量的一半时,即需要调用resize进行扩展:

if (tb->nuse >= cast(lu_int32, tb->size) && tb->size <= MAX_INT/2)
    luaS_resize(L, tb->size*2);  /* too crowded */

既然有动态扩展,也有动态缩小,在GC的时候,stringtable也会检查是否需要回收释放空间,当字符串table使用率小于50%的时候,会进行resize。

if (g->gckind != KGC_EMERGENCY) {  /* do not change sizes in emergency */
    int hs = g->strt.size / 2;  /* half the size of the string table */
    if (g->strt.nuse < cast(lu_int32, hs))  /* using less than that half? */
      luaS_resize(L, hs);  /* halve its size */
    luaZ_freebuffer(L, &g->buff);  /* free concatenation buffer */
  }

具体创建逻辑

1. 创建字符串对象:

字符串的内存结构是:TString+字符串内容+'\0','\0'是结尾标识,读取的时候只需要读一块连续内存。

 2.最最关键的地方来了,既然都创建了,那怎么存到stringtable里呢?怎么构造?好,前面的介绍其实并不是没用的,那是概览。stringtable里有一个GCObject** hash,其实就是需要通过它来索引到所有的字符串内容

hash指向hash数组,数组里每一个单元存放的是该hash值下面所有TString链表的队尾元素的地址。内存模型如下(hash值的含义实质为字符串长度,只是通过hash计算和lmod取模)。

 构造链表的函数是luaC_newobj (lgc.c):

 最后一个,前面提了很多hash关键字,在lua虚拟机里,hash算法采用的是JSHash,虚拟机启动的时候会生成一个随机种子,这个随机种子会在创建hash的时候一并带进去,以防被猜到hash值。

在Lua字符串这一块最主要的是要理解下面几个东西:
(1)Lua字符串是会被GC的
(2)每个字符串在Lua内存中只存在一份
(3)Lua通过一个全局的散列桶(stringTable)管理所有的字符串

下面一张图帮助理解一下Lua字符串存储的结构:

 4.2 lua中table的源码分析

table的定义:

typedef union TKey {
  struct {
    TValuefields;
    int next;  /* 用于标记链表下一个节点 */
  } nk;
  TValue tvk;
} TKey;

typedef struct Node {
  TValue i_val;
  TKey i_key;
} Node;


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;
  1. flags : 元方法的标记,用于查询table是否包含某个类别的元方法
  2. lsizenode : (1<<lsizenode)表示table的hash部分大小
  3. sizearray : table的数组部分大小
  4. array : table的array数组首节点
  5. node :table的hash表首节点
  6. lastfree : 表示table的hash表空闲节点的游标
  7. metatable : 元表
  8. gclist : table gc相关的参数

为了提高table的插入查找效率,在table的设计上,采用了array数组和hashtable(哈希表)两种数据的结合

所以table会将部分整形key(注意,是key,比如a[1]就是整型key,a["abc"]就是其他类型key)作为下标放在数组中, 其余的整形key和其他类型的key都放在hash表中(重点,整形放数组)

在table中的实现中,hash表占绝大部分比重,下面是table中hash表的结构示意简图:

 hash表在解决冲突有两个常用的方法:

  1. 开放定址法:当冲突发生时,使用某种探查(亦称探测)技术在散列表中形成一个探查(测)序列。沿此序列逐个单元地查找,直到找到给定 的关键字,或者碰到一个开放的地址(即该地址单元为空)为止(若要插入,在探查到开放的地址,则可将待插入的新结点存人该地址单元)。查找时探查到开放的 地址则表明表中无待查的关键字,即查找失败。
  2. 链地址法:又叫拉链法,所有关键字为同义词的结点链接在同一个单链表中。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数 组T[0…m-1]。凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。在拉链法中,装填因子α可以大于 1,但一般均取α≤1。(装填因子α:散列表中的元素个数与散列表大小的比值)

开放定址法相比链地址法节省更多的内存,但在插入和查找的时候拥有更高的复杂度。

但是table中的hash表的实现结合了以上两种方法的一些特性:

  • 查找和插入等同链地址法复杂度。
  • 内存开销近似等同于开放定址法。

原因是table中的hash表虽然采用链地址法的形式处理冲突,但是链表中的额外的节点是hash表中的节点,并不需要额外开辟链表节点;下面是TKey结构的介绍:

(也可以这样说,先用开放定址法把节点存起来,但是给节点新增指针,指向和他key值一样的节点,这样查找起来就快很多了)

在这里插入图片描述

 table表set的过程:

  • 首先调用luaH_get查找table是否已经存在这个key了,有则直接返回。
  • 否则调用luaH_newkey创建key,并返回对应的TValue。(注意此时key一定不在数组部分内)

在这里插入图片描述

 例子:

//假设tb的hash表默认大小为8个元素
local tb = {}
tb[3] = 'a'
tb[11] = 'b'
tb[19] = 'c'
tb[6] = 'd'
tb[14] = 'e'

1. 执行完local tb = {}之后,tb中的hash表状态是这样:

在这里插入图片描述

空余节点指针lastfree指向了最后一个node。

注意:table创建默认hash表大小为0,这里为了方便描述假设初始大小为8,这样就不用管rehash部分了


2. 执行完tb[3] = 'a’之后,tb中的hash表状态是这样:

在这里插入图片描述

因为3的mainposition为3,所以放在了n3位置。

注意:key的manposition等于hash(k)%table_size, 所以mainposition(3)=3%8


3. 执行完tb[11] = 'b’之后,tb中的hash表状态是这样:

在这里插入图片描述

因为11的mainposition也为3,然而3位置已经被占用了,所以此时使用lastfree获取一个空节点n7,将当前key存储在n7位置上,并且使用头插法将n7节点插入在mainposition节点n3之后,所以这里的next = n7 - n3 = 4。


4. 执行完tb[19] = 'c’之后,tb中的hash表状态是这样:

在这里插入图片描述

原因和上面类似,只不过注意的是,19是插入在mainposition节点n3和mp的next节点n7之间,所以需要重新维护n3的next值。


5. 执行完tb[6] = 'd’之后,tb中的hash表状态是这样:

在这里插入图片描述

在这一步中有些不一样的处理,首先还是算出6的mainposition为6,然后发现n6已经被key:19占用了。但是此时我们不能直接使用lastfree来存储key:6,因为19和6不是同一个链表上的,也就是说key:19抢了key:6的位置:

对于这种情况,我们需要key:19让出位置,通过lastfree申请一个空节点n5,然后将19的位置换到n5上(注意维护next节点)。然后将key:6放在n6节点上。

这部分操作就是流程图上Resetpos部分


6. 执行完tb[14] = 'e’之后,tb中的hash表状态是这样:

在这里插入图片描述

这里和步骤3类似。


下面再来分析一下Rehash的部分:

Rehash并不一定代表hash表的扩容,而是根据table里面的key的个数和类型,重新更合适的分配array的大小和hash表的大小,可能会扩容、可能不变、也有可能缩小。

假如此时table里面的array和hash表状态如下:

在这里插入图片描述

此时再插入一个key:10,因为lastfree已经无法获取空节点了,所以触发了rehash。

  1. 首先通过numusearray计算数组部分val不为nil的所有整形数目,和nums[]。对于nums[i] = j,其意义表示key在2^i-1 到2^i之间其整形key的个数有j个。
  2. 然后再通过numusehash计算hash部分Val不为nil的所有整形数目,和nums[]。
  3. 通过整形key的个数确定array的大小,computesizes,这里确定array的大小有个规则就是要满足2的幂size,并且整形key数目num > arraySize / 2,还要保证放入整形key的数目高于arraySize / 2。
  4. 最后根据array大小和总key个数,确定hash表的大小。(ps:hash表的大小也只能是2的幂,如果不是则向上对齐)

通过上面的规则可以计算得到array部分的大小为4,hash表大小为7 - 3 = 4。(7是指Key的总数,3是指能放入数组的Key的个数)

无论是array的rehash还是hash表的rehash都是先开辟新的内存,然后将原来的元素重新插入

插入key:10后的状态为:

在这里插入图片描述


值得注意的是:table元素的删除是通过table[key] = nil来实现的,然而通过我们上面对luaH_set介绍我们可以知道,仅仅是把把key对应的val设置为nil而已,并没有真正的从table中删除这个key,只有当插入新的key或者rehash的时候才可能会被覆盖或者清除。
 

5.lua数据结构关键点,重要节点,疑难杂症场景

5.1 lua在取table其长度值

lua在取table其长度值,实际上在找一个整形key,满足:

  • table[key] != nil
  • table[key+1] == nil

引用

如何深入理解Lua数据结构和内存占用? - 知乎

Lua源码分析 - 数据结构篇 - Table实现(06)_自娱自乐的代码人-CSDN博客

Lua源码分析 - 数据结构篇 - 字符串池实现(05)_自娱自乐的代码人-CSDN博客

https://blog.csdn.net/initphp/category_9293184.html

lua字符串 - 碎语心弦 - 博客园

Lua 源码分析之String_kelvin_feng的专栏-CSDN博客_lua 字符串源码(这篇逻辑清晰)

Lua string(字符串)(源码解析)_zzqlb博客-CSDN博客_lua string源码 (这篇浅显易懂)

【Lua 5.3源码】table实现分析_YzlCoder的记事本-CSDN博客 (这篇深度讲解)

  • 1
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值