从LuaJIT Bytecode介绍中可知道,Bytecode关于常量操作的指令均为D A OP
格式,其中占位最多的操作数D只有16位。如果指令操作的常数小于16位,则可以直接将其存放到操作数D的位置上,如KSHORT 0 5
,常数5占位小于16位,只需要将其存放在操作数D中即可。
但是如果指令操作的常数大于16位(大于16位的int类型和number类型),操作数D则无法容纳,这时候就需要借助一些中间结构,将常数值保存在这些中间结构中,在Bytecode生成时只需要将常量在中间结构中所对应的索引存放在操作数D中即可。如KNUM 0 0 ; 131072
,常数131072占位超过16位,将其存放在中间结构中,对应的索引是0。
在实现解释器逻辑时,KSHORT
指令的D操作数肯定是一个小于16位的常数值,直接将其使用;KNUM
指令的操作数是一个中间结构的索引,我们需要利用中间结构的基地址和索引来获取常数值。
LuaJIT中对于字符串的使用是通过引用GCRef,每产生一个新的且之前没有出现过的字符串,GC就会生成一个与之对应的不高于47位的GCRef整数值用来作为字符串的引用。GCRef和超过16位的常数值一样,将GCRef保存在中间结构,对应的索引存放在操作数D中,如KSTR 0 0 ; "13241234"
,将字符串"13241234"
的引用值GCRef存放在中间结构,对应的索引为0。
这里所说的中间结构到底是什么结构?常数值和引用值GCRef是怎么存放的呢?
中间结构其实是一个TValue类型的数组。将程序中出现的常量按出现的顺序依次且不重复的存放在数组中,将数组的index存放在Bytecode的操作数D中。这个数组有别于我们常见的数组,他的基地址指针指向的不是数组首地址,而是指向数组的中间部分。基地址指针所指位置将数组分成两段,左边用来存储GCref value,右边存储const number,如下图所示。
上面KBASE就是数组的基地址,指向数组的中间地址。对于KBASE右边(包括KBASE所指位置)存储的const number(数字常量),它的访问方式为load(KBASE + index * sizeof(TValue))
;对于KBASE左边存储的GCref value(引用常量),它的访问方式为load(KBASE - index*sizeof(TValue) - sizeof(TValue))
。现在一般都是64位的系统,所以sizeof(TValue)
的值一般都为8,index*8
相当于index<<3
,实现时可以这么写提高性能。
利用下面例子协助说明,以下是lua source:
local a = 131072;local sum = 262144;local a1 = 1310721;local a2 = 131072;local a3 = "hello world!";sum = sum + a; a = a - 1;
以下是生成的Bytecode:
0001 KNUM 0 0 ; 131072
0002 KNUM 1 1 ; 262144
0003 KNUM 2 2 ; 1310721
0004 KNUM 3 0 ; 131072
0005 KSTR 4 0 ; "hello world!"
0006 ADDVV 1 1 0
0007 SUBVN 0 0 3 ; 1
0008 RET0 0 1
Lua source中出现的常量依次是131072、262144、1310721、131072,它们均为const number,所以在解析时将131072放在了索引0处,262144放在了索引1处,1310721放在了索引2处,解析到第二个131072时,发现该常量已经出现过且在索引0处,此时只需要将0放在操作数D上即可(0004 KNUM 3 0 ; 131072
)。访问131072数值,使用load(KBASE + index * 8) = load(KBASE + 0*8) = load(KBASE)
字符串引用GCRef也是同常量一样存放在TValue类型的数组中,只不过GCRef和常量是分开存放。如上图所示中间结构TValue数组,数组的地址从左向右依次增大,KBASE是基地址。0005 KSTR 4 0 ; "hello world!"
指令表示,字符串hello world!
存放在TValue数组的左边(引用常量),索引值为0。访问该字符串,使用load(KBASE - index*8 - 8) = load(KBASE -0*8 - 8) = load(KBASE -8)
。
如何保证同一常量多次出现时只存一份(如上例中的131072)?
毋庸置疑需借用HashTable,且HashTable的元素又使用链表结构连接,方便对HashTable遍历。HashTable节点类型的定义是src/lj_obj.h
中Node结构体,val和上面说过的TValue数组的索引对应,作为常量表slot,key则是常量值或字符串的sid,next存放下一个元素的地址。src/lj_obj.h
中的GCproto->k
存放常量表基地址的值,该基地址位于数组的中间而不是首地址,在src/vm_loongarch64.dasc
文件中的BC_IFUNCF
函数中利用指令ld.d KBASE, -4+PC2PROTO(k)(PC)
将常量表基地址存放在KBASE寄存器中,GCtab->node
存放HashTable基地址。