Lua是动态类型的编程语言,变量的值可以是数值、字符串、table等所有支持的数据类型。在Lua虚拟机中每个变量都是用一个TValue结构体表示。LuaJIT出于效率的考虑重新组织了TValue结构体。
lua-5.1中的TValue结构
lua-5.1中TValue的结构定义在lobject.h中,如下所示
1 |
|
TValue结构体包含了两个部分,int类型的成员tt表示类型,Value成员是一个union结构,依据类型,有不同的含义。
- 当类型位nil时,nil本身不再需要其他标识,Value成员没有意义
- 当类型为boolean时,成员b为0或1表示false或true
- 当类型为number时,成员n表示,为double类型
- 当类型为lightuserdata时,成员p,表示指针
- 当类型为function/string/userdata/table/thread等需要GC管理的类型时,成员gc表示相应GC对象的指针。
这样一个变量只要对应一个TValue结构便可以表示Lua支持的所有类型。
lua中所有的数值都是用double类型的浮点数来表示,需要占用64位的空间。再加上额外的int类型成员tt来表示类型,一个TValue结构至少需要64+32=96位的空间,如果按照8字节对其的话就需要占用128位的空间。而LuaJIT中通过Nan-boxing技术,重组了TValue,只需要占用64位的空间。
Nan-boxing
ieee754是使用最广泛的浮点数编码格式,它将浮点数编码成三个部分,符号、指数和尾数。如下所示
双精度类型即double类型,最高位为符号位,后面的11位表示指数,最低52位为尾数。三个组合表示浮点数的值。
浮点数有些特殊的值,其中之一就是NaN(Not a Number)。有些浮点数运算如0/0得到的结果就是NaN。IEEE 754标准中,如果指数部分全为1,且尾数部分不全为0时,表示值为 NaN。double类型的浮点数尾数部分有52位,NaN只要求这52位不全为0即可,只要其中一个是1剩余的51位就可以编码表示其他的含义。
实际使用的浮点数运算单元也只会产生一种NaN表示,即0xfff8_0000_0000_0000,只用了最高的13位,剩余的的51位便可以表示Lua中其他的字符串、table等。
内存地址的处理
TValue结构中有些成员是指针,64位系统中,指针的长度为64位,那么如何在剩下的51位中表示指针类型呢?为此LuaJIT对不同类型有两种处理方式。
1. 用47位地址表示指针
对于64位系统,理论上每个进程都有64位的线性地址空间,共有16,777,216TB。然而可预见的将来,操作系统和应用并不需要这么多的内存,支持如此大的地址会增加地址转换的复杂性和成本, 因此现在的实现并不允许使用全部的地址空间。
以率先实现64位架构的AMD为例,在进行内存地址转换时,只会使用地址的低48位,并且要求从第48到63的这16位需要与第47位相同。即地址必须在0到00007FFF’FFFFFFFF 和 FFFF8000’00000000 到 FFFFFFFF’FFFFFFFF这两个范围内,共有256TB的虚拟地址空间。
操作系统本身也会对内存使用进行限制,以Linux为例,将高128TB的空间划归内核使用,这样用户态进程只能低128TB的地址,如下图所示。地址的高17位皆为0,因此使用47位即可表示所有能够使用的地址。
2. 只使用最低的2G的地址空间
以Linux为例,LuaJIT默认通过mmap系统调用来分配内存,对于x86_64平台的64位程序,mmap有一个MAP_32BIT标记选项,表示只分配虚拟地址空间的低2GB的空间,这样分配的内存地址,高33位皆为0, 相应的指针只需要32位的空间即可。
3. LuaJIT的处理与GC64模式
对于lightuserdata类型的值,LuaJIT用47位表示,对于GC类型的对象,都是通过mmap加MAP_32BIT标记分配的,用32位表示,这限制了LuaJIT只能使用不超过2GB的内存。为了摆脱这个限制,LuaJIT增加了GC64模式,开启后,所有的指针类型,包括lightuserdata都使用47位指针来表示。
LuaJIT的TValue结构
默认模式下TValue布局
LuaJIT中的TValue布局如下所示
可以通过下面的步骤判断值的类型
- 如果最高的16位(即48到63位)不全为1,表示这是一个double类型的浮点数,
- 最高的16位全为1,如果第47位为0时表示一个lightuserdata类型,
- 其余情况下,高32位表示类型,低32位表示实际值。
GC64模式下TValue布局
GC64模式开启后如下所示
这里比较特殊的是最高的13位全位1表double类型,itype表示类型,占4位,指针占用47位,总共仍是64位。
TValue结构
LuaJIT的TValue定义在lj_obj.h中,如下面所示,因为Nan-boxing的缘故,这里的TValue结构并没有直接反映实际的内存布局。
1 |
|
类型的定义
所有的数据类型定义中最高的几位均为1,方便与浮点数的区分
1 |
|
判断类型的宏定义
LuaJIT定义了一系列宏用来判断值的类型。
1 |
|
LJ_DUALNUM
Lua中数值都是double类型的浮点数,而实际使用时经常会用到整数,而位操作等都需要将double类型转换成整数进行。为此LuaJIT提供了LJ_DUALNUM的选项,一些数值可以直接通过int类型存储,方便使用,相当于为Lua增加了整数这个数据类型。
LJ_DUALNUM的定义可以参考lj_arch.h,不过对常用的x86_64架构,默认并没有启用LJ_DUALNUM。