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_GC64GCRef 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. */)};
#elsestruct {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_FR2int64_t ftsz; /* Frame type and size of previous frame, or PC. */
#elsestruct {LJ_ENDIAN_LOHI(GCRef func; /* Function for next frame (or dummy L). */, FrameLink tp; /* Link to previous frame. */)} fr;
#endifstruct {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个尾数位。
不过,浮点数中有些特殊的值,比如NaN。IEEE754规定,如果一个浮点数,指数位全为1,且尾数部分不全为0(与无穷大区分),那么它就是NaN。换句话说,只要满足这个条件,剩下的51位尾数,完全可以用来编码其他的数据。这就是NaN boxing。
有个这个先验知识,我们就能明白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_GC64uint64_t gcptr64; /* True 64 bit pointer. */
#elseuint32_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_DUALNUMo->i = (uint32_t)i; setitype(o, LJ_TISNUM);
#elseo->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