Lua源码分析之table实现

Lua简介

Lua是一个强大、高效、轻量、可嵌入的脚本编程语言,体积小、速度快,采用ANSI C语言编写,并且开放源代码。主要应用于游戏开发行业,在移动游戏开发领域,主流的引擎如Cocos2d-x、Unity都支持Lua作为脚本语言,以实现快速开发和热更新的功能。

作为一门动态语言,Lua提供了一个虚拟机,程序员创建的代码,需要通过一个代码解释器,最终转换为字节码,然后交由虚拟机解释执行。

Lua的官方版本还提供了一些常用的库,同时还提供了一系列的C API,方便第三方开发者与虚拟机交互,以及实现宿主语言(如C++)和Lua的交互。

作为一名游戏开发者,Lua是一个会经常使用到的语言。要用好一项技术,就一定要做到“知其然、知其所以然”,懂得它的实现原理,才能最大化的发挥它的优势,规避劣势。而要学习一项技术的实现原理,莫过于阅读源代码,它会告诉你最准确的信息和所有的细节。

Lua的源代码可以从其官方网站获得:https://www.lua.org/home.html

Lua中有三种最复杂和重要的数据类型:function、string和table,其中table是用户实现复杂数据结构和面向对象编程的基础。本文就基于当前Lua的最新发布版5.4.3来介绍table的实现机制,阅读代码时请注意版本差异。

源码结构

下载Lua源码后解压,会发现所有的代码都放在src文件夹下,其中一些重要文件的介绍如下:

模块 文件 描述
虚拟机核心 lapi.c C API接口
lctype.c C 标准库中的ctype相关实现
ldebug.c 调试接口
ldo.c 函数调用及堆栈管理
lfunc.c 函数原型和闭包
lgc.c 垃圾回收
lmem.c 内存管理
lobject.c 对象操作
lopcodes.c 字节码定义
lstate.c 状态机
lstring.c 字符串池
ltable.c 表的实现
ltm.c 元表的实现
lvm.c 虚拟机
lzio.c 输入流
内嵌库 lauxlib.c 辅助库函数
lbaselib.c 基础库
lcorolib.c 协程库
ldblib.c 调试库
linit.c 初始化内嵌库
liolib.c IO库
lmathlib.c 数学库
loadlib.c 动态扩展库管理
loslib.c 系统库
lstrlib.c 字符串库
ltablib.c 表处理库
源代码解析 lcode.c 代码生成器
ldump.c 序列化预编译的字节码
llex.c 词法分析器
lparser.c 解释器
lundump.c 反编译字节码
解释器和编译器 lua.c 解释器
luac.c 字节码编译器

Lua的源代码模块划分十分清晰,基本上是以一个.c为一个模块,以同名的.h文件描述导出的接口。代码风格采用K&R的风格。

Lua内部模块暴露的接口以luaX_xxx命名,其中X表示内部模块名,如string库以luaS_开头,后跟一串小写字母描述方法名。

而供外部使用的接口则以lua_xxx命名。此外还有供库开发者使用的luaL_xxx系列的接口,其余均是内部接口,禁止外部调用。

Lua的核心是虚拟机,对于用户来说,Lua虚拟机的外在数据形式是一个lua_State结构体,这个结构体代表了虚拟机当前的状态和运行所需的所有数据。调用Lua的C API均需提供这个结构体作为第一个参数。

用户编写好的代码,需通过解释器解析文本并最终生成字节码(opcode和常量),供虚拟机解释执行。

opcode的定义(lopcodes.h/lopcodes.c)和解析(lvm.c)、状态机(lstate.c)、function/table/string数据类型(lfunc.c/ltable.c/lstring.c)、内存管理(lmem.c/lzio.c)和最重要最复杂的垃圾回收(lgc.c),共同构成了虚拟机的核心。

Lua一般用来嵌入到其它系统中使用,但在官方版本里,也提供了一个独立解析器,源码在lua.c中。

闭散列

一般我们使用的哈希表,如C++的std::<unordered_map>,他们内部有一个数组,放入的元素根据key的hash值算出存储位置,如果说两个不同的key拥有同样的hash值,则称为发生了hash冲突,则通过链表或数组的形式将它们串起来,如下图所示(带颜色圆圈中的值表示的是key的hash值):

这种叫做开散列,如果发生冲突的时候,不把冲突的键值串起来,而是寻找一个空闲的位置放入新的键值,如下图所示:

在这里插入图片描述

这种叫做闭散列,Lua中的table中关于hash表部分的实现,采用的就是闭散列的算法。闭散列插入新值的过程如下:

  1. 计算key的hash值,算出存储位置
  2. 如果当前位置空闲,则插入,算法完成
  3. 如果当前位置占用,则使用probing算法,寻找空闲位置
  4. 如果找到空闲位置,则插入,算法完成
  5. 如果没有找到空闲位置,则先进行扩容和rehash,再从第1步重新开始

其中probing算法有很多种,常见的有:

  1. 线性probing:如果冲突发生在i的位置,则搜索序列为:
    - i + 1
    - i + 2
    - i + 3
    - …
  2. 二次方式probing:搜索序列为:
    - i + 1
    - i + 2x2
    - i + 3x3
    - …

每个probing算法各有所长,可以根据需要选取。

闭散列查询时的过程如下:

  1. 计算key的hash值,算出存储位置
  2. 如果当前位置空闲,则没有找到,算法完成
  3. 如果当前位置被占用,则检查key值是否相等,如果相等,则返回相应值,算法完成
  4. 如果key值不相等,则使用probing算法,寻找下一个位置
  5. 如果下一个位置的key值相等,则返回相应值,算法完成,否则返回步骤4,直到遇到空闲位置,则没有找到,算法完成

闭散列的冲突键值,是通过probing算法串起来的,有一种叫做合并散列的算法是对闭散列的进一步发展,它主要提升了发生键值聚集时的性能,它的改动有以下两项:

  1. 每个节点增加了一个next_pointer指针,指向冲突链的下一个位置
  2. probing算法为从最后一个位置往前查找

Lua的table实现对合并散列算法进行了优化,进一步提升了插入和查询时的性能,接下来介绍table的具体实现。

实现分析

Lua使用table作为统一的数据结构,这也是Lua的一大特色,通过table,用户可以方便的实现queuesetmap等数据结构。

Lua中的table有一个高效的实现,它的内部分为了数组部分和哈希部分,数组部分提供了高效且紧凑的随机访问,而其他不能放入数组部分数据则全部放入哈希表中,table的哈希表采用闭散列,实现的非常高效。

对于源码的解释,有些会单独来讲,有些会放在代码的注释里,对于直白的部分,就不再赘述了。

数据结构

首先是基础类型Value,它是Lua中所有基础类型的联合:

// Lua中所有基础类型的联合
typedef union Value {
   
  struct GCObject *gc;    /* collectable objects */
  void *p;         /* light userdata */
  lua_CFunction f; /* light C functions */
  lua_Integer i;   /* integer numbers */
  lua_Number n;    /* float numbers */
} Value;

然后是标记Value,叫做TValue,它是Lua中Value的基础表示,由一个Value和一个Tag组成:

#define TValuefields	Value value_; lu_byte tt_ //标记类型成员的宏定义,其他地方也会用到

typedef struct TValue {
   
  TValuefields;
} TValue;

#define val_(o)		((o)->value_) // 取Value的宏
#define valraw(o)	(&val_(o)) // 取value地址的宏

#define rawtt(o)	((o)->tt_) // 取标记的宏

然后是所有可回收对象的通用头和一个GCObject定义:

#define CommonHeader	struct GCObject *next; lu_byte tt; lu_byte marked

typedef struct GCObject {
   
  CommonHeader;
} GCObject;

有了以上几个定义,我们就可以看table的具体定义了,首先是节点Node的定义:

typedef union Node {
   
  struct NodeKey {
   
    TValuefields;  /* fields for value */
    lu_byte key_tt;  /* key type */
    int next;  /* for chaining */
    Value key_val;  /* key value */
  } u;
  TValue i_val;  /* direct access to node's value as a proper 'TValue' */
} Node;

#define keytt(node)		((node)->u.key_tt) // 取key标记的宏
#define keyval(node)		((node)->u.key_val) // 取key值的宏

注意这是一个union。其中key_valkey_tt组成了键值中key的值,i_valTValuefields组成了键值中Value的值。next指向冲突链的下一个位置。NodeKey之所以按照这个顺序写是为了内存对齐的考虑。

现在我们可以看看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 alimit;  /* "limit" of 'array' array */
  TValue *array;  /* array part */
  Node *node;
  Node *lastfree;  /* any free position is before this position */
  struct Table *metatable;
  GCObject *gclist;
} Table;

其中CommonHeadergclist为垃圾回收使用,其他字段的说明如下:

  • flags为元表查找方法时使用,但首位用来标识alimit是否为数组部分的真实长度
  • lsizenode为哈希部分长度的log2值,由于2的0次方为1,所以哈希部分最小长度为1
  • alimit为数组部分的限制长度,它小于等于数组部分的真实长度
  • array为数组部分
  • node为哈希部分
  • lastfree为probing时起始的位置
  • metatable指向元表

初始化和销毁

初始化通过luaH_new来进行,定义如下:

Table *luaH_new (lua_State *L) {
   
  GCObject *o = luaC_newobj(L, LUA_VTABLE, sizeof(Table));
  Table *t = gco2t(o);
  t->metatable = NULL;
  t->flags = cast_byte(maskflags);  /* table has no metamethod fields */
  t->array = NULL;
  t->alimit = 0;
  setnodevector(L, t, 0);
  return t;
}

对其中使用到的方法来逐个进行解释,首先是luaC_newobj,它负责从内存中分配一个新的对象,第二个参数指定类型,第三个指定大小。它返回的是一个GCObject,通过gco2t宏转换为Table指针,来看一下这个宏的定义:

union GCUnion {
   
  GCObject gc;  /* common header */
  struct TString ts;
  struct Udata u;
  union Closure cl;
  struct Table h;
  struct Proto p;
  struct lua_State th;  /* thread */
  struct UpVal upv;
};

#define cast_u(o)	cast(union GCUnion *, (o))
#define gco2t(o)  check_exp((o)->tt == LUA_VTABLE, &((cast_u(o))->h))

就是将GCObject对象转换为GCUnion对象,然后再通过取h成员的方法转换为Table指针。这样做的目的是由于这些集合里的对象,都拥有同样的头部结构,也就是CommonHeader,那么在任一代码位置,只要其中一个类型的完整声明是可见的,那么就可以查看这个头部中的字段。

然后设置元表flags的初始值,maskflags的定义如下:

#define maskflags	(~(~0u << (TM_EQ + 1)))

这个主要是设置了6个可以快速获取的方法的标记位。

然后是设置数组部分arrayalimit的初值。

最后是通过setnodevector初始化哈希部分,其定义如下:

static void setnodevector (lua_State *L, Table *t, unsigned int size) {
   
  if (size == 0) {
     /* no elements to hash part? */
    t->node = cast(Node *, dummynode);  /* use common 'dummynode' */
    t->lsizenode = 0;
    t->lastfree = NULL;  /* signal that it is using dummy node */
  }
  else {
   
    int i;
    int lsize = luaO_ceillog2(size); // 求size的log2值
    if (lsize > MAXHBITS || (1u << lsize) > MAXHSIZE)
      luaG_runerror(L, "table overflow");
    size = twoto(lsize); // 求2的lsize次方
    t->node = luaM_newvector(L, size, Node); // 分配内存
    // 初始化哈希部分数据
    for (i = 0; i < (int)size; i++) {
   
      Node 
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值