LuaJIT 常量数组(constant array)

从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,如下图所示。
LuaJIT 常量数组(constant array)_第1张图片

上面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基地址。

你可能感兴趣的:(LuaJIT,算法,数据结构,c++)