LuaJIT源码分析(二)数据类型

news2024/11/28 14:36:58

LuaJIT源码分析(二)数据类型

LuaJIT支持的lua数据类型和官方的lua 5.1版本保持一致,它的源文件中也有一个lua.h:

// lua.h
/*
** basic types
*/
#define LUA_TNONE		(-1)

#define LUA_TNIL		0
#define LUA_TBOOLEAN		1
#define LUA_TLIGHTUSERDATA	2
#define LUA_TNUMBER		3
#define LUA_TSTRING		4
#define LUA_TTABLE		5
#define LUA_TFUNCTION		6
#define LUA_TUSERDATA		7
#define LUA_TTHREAD		8

不过LuaJIT用来表示这些类型的通用数据结构TValue定义就和官方lua不太一样了,它的定义要复杂一些:

// lj_obj.h
/* Tagged value. */
typedef LJ_ALIGN(8) union TValue {
  uint64_t u64;		/* 64 bit pattern overlaps number. */
  lua_Number n;		/* Number object overlaps split tag/value object. */
#if LJ_GC64
  GCRef gcr;		/* GCobj reference with tag. */
  int64_t it64;
  struct {
    LJ_ENDIAN_LOHI(
      int32_t i;	/* Integer value. */
    , uint32_t it;	/* Internal object tag. Must overlap MSW of number. */
    )
  };
#else
  struct {
    LJ_ENDIAN_LOHI(
      union {
	GCRef gcr;	/* GCobj reference (if any). */
	int32_t i;	/* Integer value. */
      };
    , uint32_t it;	/* Internal object tag. Must overlap MSW of number. */
    )
  };
#endif
#if LJ_FR2
  int64_t ftsz;		/* Frame type and size of previous frame, or PC. */
#else
  struct {
    LJ_ENDIAN_LOHI(
      GCRef func;	/* Function for next frame (or dummy L). */
    , FrameLink tp;	/* Link to previous frame. */
    )
  } fr;
#endif
  struct {
    LJ_ENDIAN_LOHI(
      uint32_t lo;	/* Lower 32 bits of number. */
    , uint32_t hi;	/* Upper 32 bits of number. */
    )
  } u32;
} TValue;

首先可以看到有struct的定义有若干宏在里面,这就无形中增加了阅读的难度。我们先把这些宏给处理掉。

首先是打头的LJ_ALIGN宏,这个盲猜都能猜到,是用来控制struct内存8字节对齐用的,它在MSVC环境的定义如下:

// lj_def.h
#define LJ_ALIGN(n)	__declspec(align(n))

LJ_GC64宏会在LJ_TARGET_GC64宏生效时生效,而LJ_TARGET_GC64宏会在LuaJIT判断当前平台为64位平台时,而且没有强行禁用时生效。

// lj_arch.h
#if LUAJIT_TARGET == LUAJIT_ARCH_X86
...
#elif LUAJIT_TARGET == LUAJIT_ARCH_X64
#ifndef LUAJIT_DISABLE_GC64
#define LJ_TARGET_GC64		1
#endif
#elif LUAJIT_TARGET == LUAJIT_ARCH_ARM
...
#endif

/* 64 bit GC references. */
#if LJ_TARGET_GC64
#define LJ_GC64			1
#else
#define LJ_GC64			0
#endif

默认编译时是不会强行禁用GC64的,那么这里就可以认为LJ_GC64宏定义为1。

LJ_ENDIAN_LOHI是一个跟大小端相关的宏,而x64都是小端序:

// lj_arch.h
#if LUAJIT_TARGET == LUAJIT_ARCH_X86
...
#elif LUAJIT_TARGET == LUAJIT_ARCH_X64
#define LJ_ARCH_ENDIAN		LUAJIT_LE
#elif LUAJIT_TARGET == LUAJIT_ARCH_ARM
...
#endif

#if LJ_ARCH_ENDIAN == LUAJIT_BE
#define LJ_ENDIAN_LOHI(lo, hi)		hi lo
#else
#define LJ_ENDIAN_LOHI(lo, hi)		lo hi
#endif

LJ_FR2宏会在LJ_GC64宏生效时生效:

// lj_arch.h
/* 2-slot frame info. */
#if LJ_GC64
#define LJ_FR2			1
#else
#define LJ_FR2			0
#endif

那么根据当前的这些宏定义,我们整理一下TValue的定义:

// lj_obj.h
/* Tagged value. */
typedef __declspec(align(8)) union TValue {
  uint64_t u64;		/* 64 bit pattern overlaps number. */
  lua_Number n;		/* Number object overlaps split tag/value object. */
  GCRef gcr;		/* GCobj reference with tag. */
  int64_t it64;
  struct {
    int32_t i;	/* Integer value. */
    uint32_t it;	/* Internal object tag. Must overlap MSW of number. */
  };
  int64_t ftsz;		/* Frame type and size of previous frame, or PC. */
  struct {
    uint32_t lo;	/* Lower 32 bits of number. */
    uint32_t hi;	/* Upper 32 bits of number. */
  } u32;
} TValue;

那么,不同类型的lua数据是怎么统一都装进这个数据结构里的呢?首先,我们注意到它是一个union,而且实际大小居然只有仅仅64位。luajit为了节省空间,使用了一种名为NaN Boxing的技术。我们在luajit的源码注释中可以看到解释:

// lj_obj.h
/*
** Format for 64 bit GC references (LJ_GC64):
**
** The upper 13 bits must be 1 (0xfff8...) for a special NaN. The next
** 4 bits hold the internal tag. The lowest 47 bits either hold a pointer,
** a zero-extended 32 bit integer or all bits set to 1 for primitive types.
**
**                     ------MSW------.------LSW------
** primitive types    |1..1|itype|1..................1|
** GC objects         |1..1|itype|-------GCRef--------|
** lightuserdata      |1..1|itype|seg|------ofs-------|
** int (LJ_DUALNUM)   |1..1|itype|0..0|-----int-------|
** number              ------------double-------------
*/

IEEE754规定,64位的浮点数编码分为3个部分,1个符号位,11个指数位,以及52个尾数位。

LuaJIT源码分析(二)1

不过,浮点数中有些特殊的值,比如NaN。IEEE754规定,如果一个浮点数,指数位全为1,且尾数部分不全为0(与无穷大区分),那么它就是NaN。换句话说,只要满足这个条件,剩下的51位尾数,完全可以用来编码其他的数据。这就是NaN boxing。

LuaJIT源码分析(二)2

有个这个先验知识,我们就能明白luajit的注释了。对于普通的number,就用一个double来表示,此时64位编码就是浮点数的编码;对于其他类型,64位编码中的前13位,设定为1用来表示NaN,接下来的4位叫做itype,用来表示TValue的具体类型,不同itype的定义如下:

// lj_obj.h
/*
** ORDER LJ_T
** Primitive types nil/false/true must be first, lightuserdata next.
** GC objects are at the end, table/userdata must be lowest.
** Also check lj_ir.h for similar ordering constraints.
*/
#define LJ_TNIL			(~0u)
#define LJ_TFALSE		(~1u)
#define LJ_TTRUE		(~2u)
#define LJ_TLIGHTUD		(~3u)
#define LJ_TSTR			(~4u)
#define LJ_TUPVAL		(~5u)
#define LJ_TTHREAD		(~6u)
#define LJ_TPROTO		(~7u)
#define LJ_TFUNC		(~8u)
#define LJ_TTRACE		(~9u)
#define LJ_TCDATA		(~10u)
#define LJ_TTAB			(~11u)
#define LJ_TUDATA		(~12u)
/* This is just the canonical number type used in some places. */
#define LJ_TNUMX		(~13u)

可以看到luajit内部用到的数据类型还要更多一些。这里巧妙的是,luajit直接取反定义了这些type,这样它们的二进制表示都是1打头的,从而可以非常快速地通过移位,即可得到一个TValue的itype:

// lj_obj.h
#define itype(o)	((uint32_t)((o)->it64 >> 47))

接下来我们来看下不同的数据类型,luajit是如何在这64位中存储的。首先是number,luajit定义了numV这个宏来取出number的值,以及setnumV这个宏来设置number的值:

// lj_obj.h
#define tvisnum(o)	(itype(o) < LJ_TISNUM)
#define numV(o)		check_exp(tvisnum(o), (o)->n)
#define setnumV(o, x)		((o)->n = (x))

check_exp是luajit的一种assert的宏,当assert通过时才会执行后续的表达式,numV就是先判断TValue的类型是否为number,如果是则按union的n字段取出值。这个n字段是lua_number类型的,其实就是个double。

// luaconf.h
#define LUA_NUMBER		double

tvisnum的实现也比较巧妙,它不是直接去判断TValue是否为一个非NaN的double,而是尝试取出它的itype,如果itype比最后一个定义的LJ_TISNUM都还要小(注意itype的定义都是取反过的),那么说明它必定不是一个合法定义的TValue类型,也就只能是个double了。

再看primitive types,也就是lua里的nil,true,false,它们只有itype这4位是有效信息,后面47位的payload均为1。

// lj_obj.h
#define tvisnil(o)	((o)->it64 == -1)
#define tvisfalse(o)	(itype(o) == LJ_TFALSE)
#define tvistrue(o)	(itype(o) == LJ_TTRUE)
#define tvisbool(o)	(tvisfalse(o) || tvistrue(o))
#define boolV(o)	check_exp(tvisbool(o), (LJ_TFALSE - itype(o)))
#define setnilV(o)		((o)->it64 = -1)
#define setboolV(o, x)		((o)->it64 = (int64_t)~((uint64_t)((x)+1)<<47))

由宏定义可看出,nil的值就是-1,而-1的二进制表示即为64个1,中间4位itype就是1111,也就是~0u。类似也可以根据false和true的值算出它们的itype,分别为~1u~2u

接下来就是GC objects了。所谓GC objects,就是指lua中可被自动gc回收的对象,例如string,table类型。对于luajit,除了nil,bool,以及light userdata类型之外,其他的类型均属于GC objects。nil和bool类型是值类型,无需gc管理,而light userdata的定义就是外部管理的对象,只是将指针传给了lua,所以也不受lua的gc管理。那么这里就能看出luajit定义LJ_T顺序的讲究了,不属于GC objects的类型都定义在前面,luajit也提供了一个宏来判断一个TValue是否为GC objects:

// lj_obj.h
#define LJ_TISGCV		(LJ_TSTR+1)
#define tvisgcv(o)	((itype(o) - LJ_TISGCV) > (LJ_TNUMX - LJ_TISGCV))

这个宏设计的也很巧妙。如果是不属于GC objects的类型,例如nil和bool类型的itype,对应64位无符号整数的值,都比LJ_TISGCV要大,相减得到的值最大也就是3。而LJ_TNUMX的值要比LJ_TISGCV小,相减得到的值是负数,转换成无符号整数又会变成一个很大的值。而如果itype是属于GC objects的类型,itype对应的64位无符号整数的值,都要大于LJ_TNUMX,且小于LJ_TISGCV。最后,如果TValue是一个普通的double,那么取它的itype得到的值,一定要比LJ_TNUMX要小。luajit通过这样一个简单的宏,就能把这几种数据类型给区分开,实在是令人惊讶。

在开启LJ_GC64的情况下,从TValue中取出GC Object的宏定义如下:

// lj_obj.h
#define LJ_GCVMASK		(((uint64_t)1 << 47) - 1)
#define gcrefu(r)	((r).gcptr64)
#define gcval(o)	((GCobj *)(gcrefu((o)->gcr) & LJ_GCVMASK))

可以看到,这次用到的是TValue的gcr字段,这个字段用来保存真正GC Object的地址。在LJ_GC64下,它是个64位的地址:

// lj_obj.h
/* GCobj reference */
typedef struct GCRef {
#if LJ_GC64
  uint64_t gcptr64;	/* True 64 bit pointer. */
#else
  uint32_t gcptr32;	/* Pseudo 32 bit pointer. */
#endif
} GCRef;

不过,由于TValue前13位需要设置为全1,中间4位用来表示数据类型,实际上luajit只使用低47位的地址空间,也就是128TB,这在当今的现实世界中也绰绰有余了。

再来看看GCobj这个数据结构,它也是一个union:

// lj_obj.h
typedef union GCobj {
  GChead gch;
  GCstr str;
  GCupval uv;
  lua_State th;
  GCproto pt;
  GCfunc fn;
  GCcdata cd;
  GCtab tab;
  GCudata ud;
} GCobj;

GChead是一个通用的数据结构,用来在不知道GCobj具体类型时,获取它的通用信息。

// lj_obj.h
#define GCHeader	GCRef nextgc; uint8_t marked; uint8_t gct
typedef struct GChead {
  GCHeader;
  uint8_t unused1;
  uint8_t unused2;
  GCRef env;
  GCRef gclist;
  GCRef metatable;
} GChead;

nextgc和marked字段是用于gc管理的,gct则是用来表示GCObj类型的字段。GCHeader宏所定义的三个字段,是所有类型的GCObj所共有的。luajit会根据gct字段的值,将一个GCObj转换为实际的类型对象。

// lj_obj.h
/* Macros to convert a GCobj pointer into a specific value. */
#define gco2str(o)	check_exp((o)->gch.gct == ~LJ_TSTR, &(o)->str)
#define gco2uv(o)	check_exp((o)->gch.gct == ~LJ_TUPVAL, &(o)->uv)
#define gco2th(o)	check_exp((o)->gch.gct == ~LJ_TTHREAD, &(o)->th)
#define gco2pt(o)	check_exp((o)->gch.gct == ~LJ_TPROTO, &(o)->pt)
#define gco2func(o)	check_exp((o)->gch.gct == ~LJ_TFUNC, &(o)->fn)
#define gco2cd(o)	check_exp((o)->gch.gct == ~LJ_TCDATA, &(o)->cd)
#define gco2tab(o)	check_exp((o)->gch.gct == ~LJ_TTAB, &(o)->tab)
#define gco2ud(o)	check_exp((o)->gch.gct == ~LJ_TUDATA, &(o)->ud)

最后,我们来看下int类型。默认情况下,luajit是不开启int的,所有的数值都以double类型存储。但是在实际使用中,整数是会经常被用到的,为此luajit提供了LJ_DUALNUM的选项,一些数值可以直接通过int类型存储,方便使用。此时TValue的i字段用来保存int的值。先前提到大小端的struct在这里就发挥作用了,它保证写入int的i字段一定是TValue的后32位,同时int类型的itype则需要写入代表TValue前32位的it字段,也就是需要向左移位47 - 32 =15位。

// lj_obj.h
#define tvisint(o)	(LJ_DUALNUM && itype(o) == LJ_TISNUM)
#define intV(o)		check_exp(tvisint(o), (int32_t)(o)->i)

#define setitype(o, i)		((o)->it = ((i) << 15))

static LJ_AINLINE void setintV(TValue *o, int32_t i)
{
#if LJ_DUALNUM
  o->i = (uint32_t)i; setitype(o, LJ_TISNUM);
#else
  o->n = (lua_Number)i;
#endif
}

Reference

[1] LuaJIT的变量实现-TValue

[2] LuaJIT Internals: Intro

[3] LuaJIT GC64 模式

[4] NaN boxing or how to make the world dynamic

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

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

相关文章

将 Three 带到 Vue 生态系统,TresJs 中文文档上线

将 Three 带到 Vue 生态系统&#xff0c;TresJs 中文文档上线 中文文档上线入门指南 ThreeJS 在创建 WebGL 3D 网站方面是一个奇妙的库&#xff0c;同时他也是一个保持不断更新的库&#xff0c;一些对其封装的维护者&#xff0c;如 TroisJS&#xff0c;往往很难跟上其所有的更…

在js中本地存储的数组如何转成对象

一、此方法仅适用一维数组&#xff1b; 二、效果图 使用后 三、js代码。 function gong(s){console.log(s);let data;let kk1;// 检查ask_id是否不为空 if (s.ask_id null ) { kk1}else{kk2let dd;dds.data;sessionStorage.setItem(wenda,JSON.stringify(dd[0]))window.l…

个人简历主页搭建系列-05:部署至 Github

前面只是本地成功部署网站&#xff0c;网站运行的时候我们可以通过 localhost: port 进行访问。不过其他人是无法访问我们本机部署的网站的。 接下来通过 Github Pages 服务把网站部署上去&#xff0c;这样大家都可以通过特定域名访问我的网站了&#xff01; 创建要部署的仓库…

【Go】十三、面向对象:方法

文章目录 1、面向对象2、结构体实例的创建3、结构体之间的转换4、方法5、结构体值拷贝6、方法的注意点7、方法和函数的区别8、跨包创建结构体实例 1、面向对象 Go的结构体struct ⇒ Java的Class类Go基于struct来实现OOP相比Java&#xff0c;Go去掉了方法重载、构造函数和析构函…

[Java基础揉碎]枚举

目录 先看一个需求 枚举介绍: 枚举实现的方式: >自定义类实现枚举实例: >使用enum关键字实现枚举 ​编辑 enum关键字实现枚举注意事项 enum常用方法 enum细节 先看一个需求 要求创建季节(Season)对象&#xff0c;请设计并完成。 // 传统的方法建造一个类: clas…

企业能耗数据分析有哪些优势?怎样进行分析?

随着互联网技术的发展&#xff0c;企业在运营中会出现大量的用能数据&#xff0c;但却做不了精准的用能数据分析&#xff0c;导致数据没有得到有效利用&#xff0c;以及产生能源浪费现象。 为什么企业用能分析总是难&#xff1f; 一、用能分析过程复杂 由于用能分析过于复杂…

websocketpp上手笔记-Windows安装

WebSocketpp是什么 最近手上有一个c项目&#xff0c;需要用websocket从服务器端收内容。于是网上找了圈&#xff0c;发现WebSocketpp库可以做websocket的客户端。 WebSocketpp也叫WebSocket&#xff0c;github地址是&#xff1a;https://github.com/zaphoyd/websocketpp&…

每天五分钟深度学习:神经网络和深度学习有什么样的关系?

本文重点 神经网络是一种模拟人脑神经元连接方式的计算模型&#xff0c;通过大量神经元之间的连接和权重调整&#xff0c;实现对输入数据的处理和分析。而深度学习则是神经网络的一种特殊形式&#xff0c;它通过构建深层次的神经网络结构&#xff0c;实现对复杂数据的深度学习…

商标名称的词性:因形近不良而驳回!

近期看到业内有许多因不良驳回的案例&#xff0c;有些是直接因为不良而驳回&#xff0c;普推知产老杨发现一个因形近而驳回的案例&#xff0c;2022年5月申请的“乡巴佬”通过初审下证了&#xff0c;2022年10月申请“乡巴饶”因形近“乡巴佬”不良而驳回&#xff0c;而且还做过驳…

打造安全医疗网络:三网整体规划与云数据中心构建策略

医院网络安全问题涉及到医院日常管理多个方面&#xff0c;一旦医院信息管理系统在正常运行过程中受到外部恶意攻击&#xff0c;或者出现意外中断等情况&#xff0c;都会造成海量医疗数据信息的丢失。由于医院信息管理系统中存储了大量患者个人信息和治疗方案信息等&#xff0c;…

Word、Excel、PPT文件转PDF文件(C#)

一、添加依赖 为wpf项目引用Microsoft.Office.Interop.Excel、Microsoft.Office.Interop.PowerPoint、Microsoft.Office.Interop.Word、Office&#xff0c;依赖文件已经打到源代码包里了。 二、先定义一些命名空间 using Word Microsoft.Office.Interop.Word;using Excel M…

Vue2.x安装Tinymce依赖冲突解决

Vue2.x安装Tinymce依赖冲突原因 使用vue整合tinymce富文本编辑器&#xff0c;安装依赖时报错 报错的原因是下载版本与vue的版本对不上vue2.x版本应该使用如下指定版本依赖更合适 npm install --save "tinymce/tinymce-vue^3.1"额外依赖为 npm install --save &quo…

ssm013小型企业办公自动化系统的设计和开发+vue

小型企业办公自动化系统的设计与实现 摘 要 互联网发展至今&#xff0c;无论是其理论还是技术都已经成熟&#xff0c;而且它广泛参与在社会中的方方面面。它让信息都可以通过网络传播&#xff0c;搭配信息管理工具可以很好地为人们提供服务。针对小型企业办公信息管理混乱&am…

流行的API架构学习

几种流行的API架构风格图 SOAP&#xff08;Simple Object Access Protocol&#xff09; 优点&#xff1a;SOAP 是一种基于 XML 的通信协议&#xff0c;具有良好的跨平台和跨语言支持。它提供了丰富的安全性和事务管理功能&#xff0c;并支持复杂的消息交换模式。 缺点&#xf…

远控桌面多任务并发文件保密传输

远程桌面文件传输是一个重要的功能&#xff0c;大多数远控都是用的桌面程序模式&#xff0c;利用系统自带复制粘贴拖拽文件拷贝功能&#xff0c;做一个ole调用对接&#xff0c;可以将很多控制权交给操作系统。 但我做的是浏览器版&#xff0c;浏览器是沙盒原理&#xff0c;为了…

mkcert生成ssl证书+nginx部署局域网内的https服务访问问题

文章目录 mkcert生成ssl证书nginx部署局域网内的https服务访问问题1、下载mkcert查看自己的电脑是arm还是amd架构 2、安装mkcert3、测试mkcert是否安装成功4、查看CA证书存放位置5、打开windows的证书控制台6、生成自签证书,可供局域网内使用其他主机访问以下是nginx部署https服…

代码随想录第27天 | 39. 组合总和、40.组合总和II、131.分割回文串

一、前言 今天的主题还是回溯算法&#xff0c;还是根据那个backtracking模板&#xff0c;但是今天会涉及到去重和一些小细节的问题。 二、组合总和 1、思路&#xff1a; 我一开始的想法就是在for循环转化为&#xff1a; for(int i 0; i < size; i) 但是这个是会陷入一…

【LDLTS】拉普拉斯深能级瞬态光谱

Laplace deep level transient spectroscopy&#xff08;拉普拉斯深能级瞬态光谱&#xff0c;简称LDLTS&#xff09;是一种用于分析和表征半导体材料中深层能级缺陷的技术。它是传统能级瞬态光谱&#xff08;DLTS&#xff09;方法的扩展和改进&#xff0c;特别适用于解决传统DL…

Kubernetes kafka系列 | Strimzi 部署kafka-bridge

Strimzi kafka集群部署直通车 一、kafka bridge 介绍 Kafka Bridge 是 Apache Kafka 生态系统中的一个工具或组件&#xff0c;用于实现 Kafka 与其他系统或协议之间的通信或集成。Kafka 本身是一个分布式事件流平台&#xff0c;广泛用于构建实时数据流水线和流式应用程序。然而…

【操作系统】想要更好的学习计算机,操作系统的知识必不可少!!!

操作系统的概念 导言一、日常生活中的操作系统二、计算机系统层次结构三、操作系统的定义3.1 控制和管理计算机资源3.2 组织、调度计算机的工作与资源的分配3.3 给用户和其他软件提供方便接口与环境3.4 总结 四、操作系统的目标和功能4.1 作为管理者4.1.1 处理机管理4.1.2 存储…