LuaJIT:Bytecodes介绍

一、Introduction

关于Bytecode介绍的官方文档:http://wiki.luajit.org/Bytecode-2.0
最近发现作者将Bytecode和SSA IR的介绍的文档删除了,也不知道是为什么。

发现Tarantool的wiki中保存着Bytecode原来的官方介绍:https://github.com/tarantool/tarantool/wiki/LuaJIT-Bytecodes

在LuaJIT的源码中,关于Bytecode的指令格式的定义,在src/lj_bc.h中。

在使用LuaJIT时,可以加上参数-bl可以列出所执行lua脚本生成的ByteCode,如:luajit -bl test.lua

指令格式:

ByteCode指令占4个byte(32bit),有两种格式:一种是有ABC三个操作数,一种是只有AD两个操作数。A一般是dst操作数,占8位;B、C为src操作数,占8位;D一般是src操作数,占16位。

/* Bytecode instruction format, 32 bit wide, fields of 8 or 16 bit:
**
** +----+----+----+----+
** | B  | C  | A  | OP | Format ABC
** +----+----+----+----+
** |    D    | A  | OP | Format AD
** +--------------------
** MSB               LSB
**
** In-memory instructions are always stored in host byte order.
*/

假设0xbbccaa1e是一条ByteCode编码,则:op = 0x1e (src/lj_bc.h中有一个枚举类型BCOp,从中可以查看具体值对应的opcode),A = 0xaa,B = 0xbb,C = 0xcc;或者op = 0x1e,A = 0xaa,D = bbcc

src/lj_bc.h定义了get/set指令操作数和操作码的的宏定义。

/* Macros to get instruction fields. */
#define bc_op(i)        ((BCOp)((i)&0xff))
#define bc_a(i)         ((BCReg)(((i)>>8)&0xff))
#define bc_b(i)         ((BCReg)((i)>>24))
#define bc_c(i)         ((BCReg)(((i)>>16)&0xff))
#define bc_d(i)         ((BCReg)((i)>>16))
#define bc_j(i)         ((ptrdiff_t)bc_d(i)-BCBIAS_J)

/* Macros to set instruction fields. */
#define setbc_byte(p, x, ofs) \
  ((uint8_t *)(p))[LJ_ENDIAN_SELECT(ofs, 3-ofs)] = (uint8_t)(x)
#define setbc_op(p, x)  setbc_byte(p, (x), 0)
#define setbc_a(p, x)   setbc_byte(p, (x), 1)
#define setbc_b(p, x)   setbc_byte(p, (x), 3)
#define setbc_c(p, x)   setbc_byte(p, (x), 2)
#define setbc_d(p, x) \
  ((uint16_t *)(p))[LJ_ENDIAN_SELECT(1, 0)] = (uint16_t)(x)

指令定义:

关于指令的定义,在下面的这个结构中有说明,#define BCDEF(_)这个宏中定义了每个指令格式和其操作数类型,可以查阅。

/* Bytecode opcode numbers. */
typedef enum {
#define BCENUM(name, ma, mb, mc, mt)    BC_##name,
BCDEF(BCENUM)
#undef BCENUM
  BC__MAX
} BCOp;

指令名后面会跟个后缀,用于区分操作数B、C或D的不同类型。如ADDVNADDNV:前者表示dst = V + N,即操作数B是一个变量,操作数C是一个常量;后者表示dst = N + V,即操作数B是一个常量,操作数C是一个变量。这种后缀代表含义如下:

/*
** The opcode name suffixes specify the type for RB/RC or RD:
** V = variable slot
** S = string const
** N = number const
** P = primitive type (~itype)
** B = unsigned byte literal
** M = multiple args/results
*/

V对应的操作数类型是变量,LuaJIT用一个Stack维护VM的运行时,variable slot表示的是变量在Stack中的index,关于var stack的介绍请阅读《LuaJIT 栈帧布局(stack frames layout)》。

N和S表示对应操作数类型是number和string。string操作数的中的值肯定是string在其存放结构中的index;number如果是位数小于16位的整数,则操作数的值是number字面值,否则就是number值存放结构中的index。其实string和number存放在同一个结构中,我把它叫做常量数组,同Stack一样都是很重要的结构,关于常量数组的介绍请阅读《LuaJIT 常量数组(constant array)》。

P表示操作数是一个primitive类型,B就是一个字节的字面值,M表示多个args/results.

二、Comparison、JMP、Test and Copy

Comparison、test及copy指令后面会紧跟一条JMP跳转指令,如果comparison或test的运算结果为trueJMP会跳转到指定的目标处,否则就会继续执行JMP之后的指令。

2.1 Comparison

comparison指令语法为OP A D,用于求操作数AD的关系运算结果,所有操作在条件为真时分支执行,否则直接向下执行。当comparison指令的运算结果为true时,JMP的跳转目标(jump)是对应的是if语句之外的语句或者else块中的语句。比如source code内容为:if(a > b) then stat1; end;,则对应的指令如下:

ISGE     b   a
JMP      target
stat1
...
OPADDescription
ISLTvarvarJump if A < D
ISGEvarvarJump if A ≥ D
ISLEvarvarJump if A ≤ D
ISGTvarvarJump if A > D
ISEQVvarvarJump if A = D
ISNEVvarvarJump if A ≠ D
ISEQSvarstrJump if A = D
ISNESvarstrJump if A ≠ D
ISEQNvarnumJump if A = D
ISNENvarnumJump if A ≠ D
ISEQPvarpriJump if A = D
ISNEPvarpriJump if A ≠ D
JMPrbasejumpJump

2.2 JMP

JMP 指令的操作数A,它存储第一个未使用的槽位。操作数D则是跳转目标jump,对其解释为: branch target, relative to next instruction, biased with 0x8000

  • branch target(分支目标):指的是跳转目标,即程序将要跳转到的指令位置。
  • relative to next instruction(相对于下一条指令):跳转的目标地址是 相对地址,不是绝对地址。也就是说,目标地址是基于 当前指令的下一条指令 来计算的,而不是一个固定的内存地址。
  • biased with 0x8000(偏移 0x8000):表示跳转偏移值是带有 0x8000(32768) 的偏移基准。这意味着,偏移值是一个 有符号数,即可以是正数(向前跳转)或负数(向后跳转)。
    • 在编码时,跳转偏移会在存储前被 加上 0x8000,使得它变成无符号数,方便存储和解析。在执行时,需要 减去 0x8000 以恢复原始的相对偏移值。
    • 假设某条跳转指令的偏移字段值是 0x8010,那么存储时,它实际表示的偏移量 = 0x8010 - 0x8000 = 0x10(即 16),这意味着程序将跳转到 下一条指令之后的第 16 条指令 处。
    • 如果偏移字段是 0x7FF0,那么存储的值 = 0x7FF0 - 0x8000 = -16,这表示程序 向后跳转 16 条指令
    • 这么做的原因是:指令中的跳转偏移量是一个 有符号数,但Bytecode的RD操作数是以无符号数形式存储的。通过 加 0x8000,我们可以将一个 有符号的 -32768 ~ 32767 的范围 转换成 无符号的 0 ~ 65535,从而在 16 位字段 内表示 正负偏移,而无需额外的符号位。解码操作时,也不需要符号扩展,只要0x8000即可。

2.3 Test and Copy

test和copy指令用于检查一个boolean变量在上下文中的计算结果,如果计算结果是nilfalse,指令认为值为false,其他任何计算结果指令都认为是true

copy指令ISTC A D:如果Dtrue,copy D的值到A中,执行后面紧跟的JMP指令跳转到指定的目标处,否则不跳转继续执行JMP之后的指令。指令ISFC A D则是在D为false时,先copy再jmp。

  /* copy ops. */ \
  _(ISTC,   dst,    ___,    var,    ___) \	// if D==true,则A=D且执行JMP
  _(ISFC,   dst,    ___,    var,    ___) \	// if D==false,则A=D且执行JMP

test指令IST D,如果Dtrue,执行后面紧跟的JMP指令跳转到指定的目标处,否则不跳转继续执行JMP之后的指令。指令ISTYPE A D,A是个变量,D是类型值,如果A的类型值itype,后面会专门写一片文章讲TValue结构,会涉及到这个知识)和D的值相等,则啥也不干,否则将A的类型转换为D的类型。ISNUM与ISTYPE类似。

  /* Unary test ops. */ \
  _(IST,    ___,    ___,    var,    ___) \	// if D == true, 则执行JMP
  _(ISF,    ___,    ___,    var,    ___) \	// if D == false, 则执行JMP
  _(ISTYPE, var,    ___,    lit,    ___) \
  _(ISNUM,  var,    ___,    lit,    ___) \

三、Unary and Binary ops

3.1 Unary

一元运算指令有4条。MOV A D将D中的值复制到A中;NOT A D对D中的值做按位逻辑非运算,运算结果放入A;UNM是对D中的值取反;LEN是求字符串或table的长度。

OPADDescription
MOVdstvarCopy D to A
NOTdstvarSet A to boolean not of D
UNMdstvarSet A to -D (unary minus)
LENdstvarSet A to #D (object length)

3.2 Binary

二元运算指令有17条,上文介绍指令定义 中有使用过这部分的例子。基本的运算是加减乘除取余POW a b c => a = b^cCAT表示字符串连接操作,将B和C连接,结果放入A。

OPABCDescription
ADDVNdstvarnumA = B + C
SUBVNdstvarnumA = B - C
MULVNdstvarnumA = B * C
DIVVNdstvarnumA = B / C
MODVNdstvarnumA = B % C
ADDNVdstvarnumA = C + B
SUBNVdstvarnumA = C - B
MULNVdstvarnumA = C * B
DIVNVdstvarnumA = C / B
MODNVdstvarnumA = C % B
ADDVVdstvarvarA = B + C
SUBVVdstvarvarA = B - C
MULVVdstvarvarA = B * C
DIVVVdstvarvarA = B / C
MODVVdstvarvarA = B % C
POWdstvarvarA = B ^ C
CATdstrbaserbaseA = B … C

四、Upvalue and function ops

这里介绍Lua中有关于函数操作相关的两个概念,知道这两个概念再去理解相关指令就很好理解:一个是外部局部变量,也称为upvalue,另一个是closure,翻译过来叫闭包

4.1 外部局部变量(upvalue)

Lua 中的函数是一阶类型值(first-class value),定义函数就象创建普通类型值一样(只不过函数类型值的数据主要是一条条指令而已),所以在函数体中仍然可以定义函数。假设函数f2定义在函数f1中,那么就称f2f1的内嵌(inner)函数,f1f2的外包(enclosing)函数,外包和内嵌都具有传递性,即f2的内嵌必然是f1 的内嵌,而f1的外包也一定是f2的外包。内嵌函数可以访问外包函数已经创建的所有局部变量,这种特性便是所谓的词法定界(lexical scoping),而这些局部变量则称为该内嵌函数的外部局部变量(external local variable)或者upvalue(是变量而不是值)。

 19 function f1(n)
 20   local function f2()
 21     print(n)
 22   end
 23 return f2
 24 end
 25
 26 do
 27   g1 = f1(1024)
 28   g1()
 29 end

-- BYTECODE -- test_while.lua:20-22
0001    GGET     0   0      ; "print"
0002    UGET     2   0      ; n
0003    CALL     0   1   2
0004    RET0     0   1

-- BYTECODE -- test_while.lua:19-24
0001    FNEW     1   0      ; test_while.lua:20
0002    UCLO     0 => 0003
0003 => RET1     1   2

-- BYTECODE -- test_while.lua:0-30
0001    FNEW     0   0      ; test_while.lua:19
0002    GSET     0   1      ; "f1"
0003    GGET     0   1      ; "f1"
0004    KSHORT   2 1024
0005    CALL     0   2   2
0006    GSET     0   2      ; "g1"
0007    GGET     0   2      ; "g1"
0008    CALL     0   1   1
0009    RET0     0   1

函数f1的参数n是函数f2的upvalue,当执行完g1 = f1(1024)后,局部变量n的生命周期本该结束,但因为它已经成了内嵌函数f2的upvalue,并且它又被赋给了变量g1,所以它仍然能以某种形式继续“存活”下来,从而令g1()打印出正确的值。

4.2 闭包(closure)

Lua解析一个函数编译生成字节码时,会为它生成一个原型(prototype),其中包含了函数体对应的ByteCode指令、函数用到的常量值(数字字面量,文本字符串等等)和一些调试信息。在运行时,每当Lua执行一个形如function...end 这样的表达式时,它就会根据原型创建一个新的数据对象(ByteCode的FNEW指令),其中包含了相应函数原型的引用、环境(environment,用来查找全局变量的表)的引用以及一个由所有upvalue引用组成的数组,而这个数据对象就称为闭包。由此可见,函数是编译期概念,是静态的,而闭包是运行期概念,是动态的。

这部分操作的指令总共有7条,除了FNEW之外其余都是以U开头。

OPADDescription
UGETdstuvSet A to upvalue D
USETVuvvarSet upvalue A to D
USETSuvstrSet upvalue A to string constant D
USETNuvnumSet upvalue A to number constant D
USETPuvpriSet upvalue A to primitive D
UCLOrbasejumpClose upvalues for slots ≥ rbase and jump to target D
FNEWdstfuncCreate new closure from prototype D and store it in A

对上面这几条指令逐一解释:

  • UGET dst uv指令:将upvale值uv复制到solt dst中。注意按照指令格式与dst(对应A)和uv(对应D)对位,后面也是。
  • USETV uv var指令:将solt var中的值复制到uv,这一条指令同上一条指令不一样,需要考虑GC,即三色标记的白色不能赋值给黑色这一问题。
  • USETSUSETNUSETP同USETV一样,只是类型不同而已。
  • UCLO rbase jump指令:close栈上满足条件slots ≥ rbase的所有upvalue值,upvalue有两种状态,open和close。
  • FNEW dst func指令:根据函数原型func创建一个函数闭包,放进solt dst中。该指令一般在函数调用时会使用,如调用一个自定义lua函数之前,需要创建函数的闭包。

五、Table ops

Lua中的table是一个关联数组,除了nil以外的任何值都可以做为key,大小不固定,可动态扩容。

这里说一下关联数组哈希表的区别:哈希表在存值时,必须使用key值,你可以将key设置位123这种类似于数组index的形式,但必须有key,如array = {"123" = "Lua", "456" = "Tutorial"};关联数组则同时兼具了普通数组和哈希表的功能,普通数组可以在存值时不用index(key),只要有value,会有默认的index,如array = {"Lua", "Tutorial"}

OPABC/DDescription
TNEWdstlitSet A to new table with size D (see below)
TDUPdsttabSet A to duplicated template table D
GGETdststrA = _G[D]
GSETvarstr_G[D] = A
TGETVdstvarvarA = B[C]
TGETSdstvarstrA = B[C]
TGETBdstvarlitA = B[C]
TSETVvarvarvarB[C] = A
TSETSvarvarstrB[C] = A
TSETBvarvarlitB[C] = A
TSETMbasenum*(A-1)[D], (A-1)[D+1], … = A, A+1, …

配合下面给出的解释和例子,对上面这几条指令逐一解释:

  • TNEW指令的D操作数占16位,低11位存放数组size,也就是要将table当作普通数组使用;高5位存放一个叫做hsize的值,也就是要将table当作哈希表使用,用2^hsize结果作为哈希表的大小。
  • GGETGSET指令,可以理解为是在当前函数执行环境的全局符号表中获取或存放对象,_G就是全局符号表。
  • TGETSTSETS等指令是以C作为B的index存取值,下面例子中的指令TGETS 0 0 5 ; "sort"的意思是:在名字是table的table中取出key = sort的value。
  • TSETM指令,一般在有变参的函数中使用,solt(A-1)中存放的肯定是个table,如下图所示,TSETM需要做table[val] = solt Atable[val + 1] = solt(A + 1)table[val] = solt(A + 2)……直到MULTRES为零的时候,赋值结束。
    在这里插入图片描述

这是一个对table操作的例子:

do
  fruits = {"banana","orange","apple","grapes"}
  for k,v in ipairs(fruits) do
          print(k,v)
  end
  table.sort(fruits)
  for k,v in ipairs(fruits) do           
            print(k,v)
  end
end

0001    TDUP     0   0
0002    GSET     0   1      ; "fruits"
0003    GGET     0   2      ; "ipairs"
0004    GGET     2   1      ; "fruits"
0005    CALL     0   4   2
0006    JMP      3 => 0011
0007 => GGET     5   3      ; "print"
0008    MOV      7   3
0009    MOV      8   4
0010    CALL     5   1   3
0011 => ITERC    3   3   3
0012    ITERL    3 => 0007
0013    GGET     0   4      ; "table"
0014    TGETS    0   0   5  ; "sort"
0015    GGET     2   1      ; "fruits"
0016    CALL     0   1   2
0017    GGET     0   2      ; "ipairs"
0018    GGET     2   1      ; "fruits"
0019    CALL     0   4   2
0020    JMP      3 => 0025
0021 => GGET     5   3      ; "print"
0022    MOV      7   3
0023    MOV      8   4
0024    CALL     5   1   3
0025 => ITERC    3   3   3
0026    ITERL    3 => 0021
0027    RET0     0   1

六、Calls and vararg handling

函数调用相关指令总共有8条,有多参数的,迭代器相关的以及变参的,T = tail call

OPABC/DDescription
CALLMbaselitlitCall: A,…,A+B-2 = A(A+1,…,A+C+MULTRES)
CALLbaselitlitCall: A,…,A+B-2 = A(A+1,…,A+C-1)
CALLMTbaselitTailcall: return A(A+1,…,A+D+MULTRES)
CALLTbaselitTailcall: return A(A+1,…,A+D-1)
ITERCbaselitlitCall iterator: A, A+1, A+2 = A-3, A-2, A-1; A,…,A+B-2 = A(A+1, A+2)
ITERNbaselitlitSpecialized ITERC, if iterator function A-3 is next()
VARGbaselitlitVararg: A,…,A+B-2 = …
ISNEXTbasejumpVerify ITERN specialization and jump
  • 所有的调用指令都需要特殊的设置:被调用的函数(或对象)位于slot A,后续连续的slot中是参数;操作数 B = 返回值数量 + 1,若B为 0,则表示返回所有结果(并设置 MULTRES 变量);操作数 C = 固定参数数量 + 1。
  • 对于有多个参数的调用(CALLM 或 CALLMT),操作数 C 表示固定参数数量,实际传递参数的数量 = 固定参数数量 + MULTRES。这些特殊的调用形式会将 MULTRES(上一条指令返回的值数量)也加入参数中,用来支持 f(a, b, unpack(t)) 这类语法。
  • 为了保持一致性,特化的调用指令 ITERCITERN 和可变参数指令 VARG 使用相同的的操作数格式。ITERCITERN 的操作数 C 始终是 3(即 1+2),表示传递 2 个参数给迭代器函数。VARG 的 C 被重用来存储外层函数的固定参数数量,这样可以更快地访问伪帧中的可变参数部分。VARG 的操作数 C 被重新用于保存封闭函数的固定参数数量。这加快了对下面可变参数伪框架的可变参数部分的访问速度。
  • MULTRES 是一个内部变量,用于记录前一次调用或 VARG 返回的结果数量。它被 CALLM / CALLMTRETM(多返回值)和表初始化器(TSETM)使用。

优化:pairs() 和 next() 的识别

Lua 的解析器会启发式地判断某处循环中是否可能使用了 pairs()next(),如果是,它会把原本的 JMP 和迭代器调用 ITERC 替换为特化指令 ISNEXTITERNISNEXT 会在运行时检查以下条件是否满足:

  • 迭代器确实是 next() 函数;
  • 参数是一个表;
  • 控制变量是 nil

如果满足,它就会将控制变量的slot的低 32 位清零,并跳转到迭代器调用,用该数字来高效遍历表的键。如果这些假设不成立,字节码会在运行时“反特化”,重新回到常规的 JMPITERC

概括来说,Lua 通过静态分析加速常见的 for k,v in pairs(t) 循环,如果能确定用的是 next() 并且是标准形式,就用特化指令加速执行;如果运行时发现情况不符合,它还能“降级”回原始形式。

下例是一个函数调用的实例,对其中使用到的指令做出如下解释:

 43 function add(a, b)
 44   return a + b;
 45 end
 46 do
 47   local a = 111;
 48   local b = 222;
 49   local c = add(a, b);
 50   --print(c);
 51 end

-- BYTECODE -- test_while.lua:43-45		
0001    ADDVV    2   0   1
0002    RET1     2   2

-- BYTECODE -- test_while.lua:0-58
0001    FNEW     0   0      ; test_while.lua:43  
0002    GSET     0   1      ; "add"		
0003    KSHORT   0 111					
0004    KSHORT   1 222					
0005    GGET     2   1      ; "add"		
0006    MOV      4   0					
0007    MOV      5   1					
0008    CALL     2   2   3				
0009    RET0     0   1

第一个Bytecode lua:43-45为函数add的原型,在原型中参数参数默认依次放在slot 0、1...中。

  • ADDVV:slot 2 = slot 0 + slot 1。
  • RET1:第一个参数是存放返回值的slot number,第二个参数是返回值个数+1

第二个Bytecode lua:0-58do...end块的字节码,是从这里创建要给add函数的闭包,再去调用函数的。

  • FNEW:根据slot 0中存放的函数原型,创建一个闭包并存放到slot 0中。
  • GSET:将slot 0中存放的闭包放到全局符号表中,key为add。
  • KSHORT:将常量111222存放到slot 0、slot 1中。
  • GGET:以key=add,取出全局符号表中存放给的闭包存放到slot 2。
  • MOV:copy slot 0 to slot 4,copy slot 1 to slot 5。
  • CALL:第一个参数为被调函数闭包slot number,第二个参数为返回值个数+1的常数值,第三个参数为被调函数参数个数+1的常数值。
  • RET0:这是虚拟机自己补的一条指令,把do...end当作一个函数来处理,编译器前端基本上都是这么去处理,把source最外层的一个块当作中间码的一个函数去处理。RET0只改变控制流程,返回值个数为0

这里再说几个官方文档中的一些不好理解的点。如CALL指令的A, ..., A+B-2 = A(A+1, ..., A+C-1),现在的LuaJIT版本已经这种结构,而是A, ..., A+B-2 = A(A+2, ..., A+C),其中solt A存放callee函数,B表示返回值个数+1,C表示参数个数+1,结构var stack如图所示:
在这里插入图片描述

比如说call 2 2 3,调用前statck上A处(slot 2)存放被调函数的闭包,实参是从A+2开始,一直到A+C(A+3);调用结束后,参数个数为2-1=1个,第一个返回值存放在statck上A处(slot 2)。

七、Return

RET比较好理解,总共有4条,主要是将被掉函数的返回结果传到主调函数调用帧中的规定位置。

OPADDescription
RETMbaselitreturn A, …, A+D+MULTRES-1
RETrbaselitreturn A, … A+D-2
RETOrbaselitreturn
RET1rbaselitreturn A
  • 所有返回指令都会把从slot A 开始的返回结果,复制到从base slot下面两个slot位开始的位置(base-16)。

  • RET0 和 RET1 指令是 RET 指令的特殊版本,前者返回值个数为0,后者返回值个数为1。

  • 操作数 D 的值等于“要返回的结果数 + 1”。例如,如果返回 0 个结果,那么 D 的值是 1;返回 1 个结果时,D 为 2,以此类推。

  • 对于 RETM 指令,操作数 D 表示固定返回值的数量。另外,还有一个内部变量 MULTRES,它保存了上一次调用或 vararg 指令返回的多结果数量。实际返回的结果数是:操作数 D 加上 MULTRES 的值。

RET A D(return A, ..., A+D-2),A是存放第一个返回值的slot number,D是返回值个数+1。从slot A连续开始,一直到 slot A+D-2都是要返回的值。slot A的值会被复制到base-16的位置,其他值依次按序赋值。

八、Loops and branches

Lua中有4种循环方式,3种基本的循环语句,1种用于迭代器的循环。根据下图种给出的例子对照阅读。

  • 1)for循环: for i=start,stop,step do body end => set start,stop,step FORI body FORL

    local a = 5;
    local sum = 0;
    for i = 1, 5, 1
      do sum = sum + i;
    end
    
    -- BYTECODE --
    0001    KSHORT   0   5
    0002    KSHORT   1   0
    0003    KSHORT   2   1
    0004    KSHORT   3   5
    0005    KSHORT   4   1
    0006    FORI     2 => 0009
    0007 => ADDVV    1   1   5
    0008    FORL     2 => 0007
    0009 => RET0     0   1
    
  • 2)while循环: while cond do body end => inverse-cond-JMP LOOP body JMP

    local a = 5;
    local sum = 0;
    while (a > 1) do
      sum = sum + a;
      a = a - 1;
    end  
    
    -- BYTECODE --
    0001    KSHORT   0   5
    0002    KSHORT   1   0
    0003 => KSHORT   2   1
    0004    ISGE     2   0
    0005    JMP      2 => 0010
    0006    LOOP     2 => 0010
    0007    ADDVV    1   1   0
    0008    SUBVN    0   0   0  ; 1
    0009    JMP      2 => 0003
    0010 => RET0     0   1
    
  • 3)repeat循环: repeat body until cond => LOOP body cond-JMP

    local a = 5;
    local sum = 0;
    repeat
      a = a + 1;
      sum = sum + a;
    until(a > 5)
    
    -- BYTECODE --
    0001    KSHORT   0   5
    0002    KSHORT   1   0
    0003 => LOOP     2 => 0009
    0004    ADDVN    0   0   0  ; 1
    0005    ADDVV    1   1   0
    0006    KSHORT   2   5
    0007    ISGE     2   0
    0008    JMP      2 => 0003
    0009 => RET0     0   1
    
  • 4)迭代器: for vars... in iter,state,ctl do body end => set iter,state,ctl JMP body ITERC ITERL

    fruits = { "banana","orange","apple","grapes"}
    for k,v in ipairs(fruits) do
      print(k,v);
    end
    
    -- BYTECODE -- 
    KGC    0    table
    KGC    1    "fruits"
    KGC    2    "ipairs"
    KGC    3    "print"
    0001    TDUP     0   0
    0002    GSET     0   1      ; "fruits"
    0003    GGET     0   2      ; "ipairs"
    0004    GGET     2   1      ; "fruits"
    0005    CALL     0   4   2
    0006    JMP      3 => 0011
    0007 => GGET     5   3      ; "print"
    0008    MOV      7   3
    0009    MOV      8   4
    0010    CALL     5   1   3
    0011 => ITERC    3   3   3
    0012    ITERL    3 => 0007
    0013    RET0     0   1
    
  • 5)breakgoto 语句会被转换为 无条件JMPUCLO 指令。

循环和分支跳转相关的指令总共有12条,会涉及到切换JIT模式的热点跟踪和计数。breakgoto语句被转换成了无条件CMPUCLO

OPADDescription
FORIbasejumpNumeric for loop init
JFORIbasejumpNumeric for loop init, JIT-compiled
FORLbasejumpNumeric for loop
IFORLbasejumpNumeric for loop, force interpreter
JFORLbaselitNumeric for loop, JIT-compiled
ITERLbasejumpIterator for loop
IITERLbasejumpIterator for loop, force interpreter
JITERLbaselitIterator for loop, JIT-compiled
LOOPrbasejumpGeneric loop
ILOOPrbasejumpGeneric loop, force interpreter
JLOOPrbaselitGeneric loop, JIT-compiled
JMPrbasejumpJump

对上面几条指令作出简单解释。

  • 1)操作数 A 存储不同指令的基址:
    对于 JMP 指令,它存储第一个未使用的槽位。
    对于 *FOR* 指令,它存储 循环控制变量idxstopstep、扩展索引 ext idx)的基址。
    对于 *ITERL 指令,它存储 迭代器返回的结果的基址(包含 funcstatectl)。

  • 2)JFORL、JITERL 和 JLOOP 指令
    这些指令的操作数 D 存储 JIT 追踪编号(trace number)JFORI 指令则从对应的 JFORL 指令中获取该编号。
    如果没有 JIT 追踪编号,操作数 D 指向循环后的第一条指令

  • 3)FORL、ITERL 和 LOOP 指令
    负责热点检测(hotspot detection),即判断循环是否执行足够频繁。
    如果循环被多次执行,则 触发 JIT 追踪(trace recording) 进行优化。

  • 4)IFORL、IITERL 和 ILOOP 指令:这些指令由 JIT 编译器用于屏蔽(blacklist)无法编译的循环不会进行热点检测,强制该循环在 解释器(interpreter)模式下执行

  • 5)JFORI、JFORL、JITERL 和 JLOOP 指令:如果 循环入口条件为真,则进入 JIT 编译的追踪(JIT-compiled trace)

  • 6)FORL 指令 执行流程:
    a. 首先执行 idx = idx + step
    b. 检查循环条件:如果 step >= 0,则检查 idx <= stop;如果 step < 0,则检查 idx >= stop
    c. 如果条件为真idx 被复制到 扩展索引槽位(ext idx),作为循环体内可见的循环变量;进入循环体 进入 JIT 编译的追踪
    d. 否则:跳出循环,继续执行 FORL 之后的下一条指令。

  • 7)ITERL 指令 执行流程:
    a. 检查槽 A 中迭代器返回的第一个结果是否为非 nil
    b. 如果结果非 nil:该值被复制到 slot A-1,作为循环体内的迭代变量;进入循环体 进入 JIT 编译的追踪

  • 8)LOOP 指令
    本质上是一个空操作(no-op),除了用于热点检测,不会执行跳转
    操作数 A 和 D 仅供 JIT 编译器使用,以加速 数据流(data-flow)和控制流(control-flow)分析
    JIT 编译器需要该指令,以便在 循环优化时进行patch修改,让循环进入 JIT 编译的追踪。

九、Function headers

在VM执行每个函数之前,都会执行这部分BC。函数调用需要增加Lua Stack,所以检查Stack是否够用是这部分的工作。还需要检查函数参数是否匹配,比如有的形参个数是3个,实参个数只有两个,这时候就要给缺少的那个形参的位置填充一个值(nil)。

前缀I/J = interp/JIT,后缀F/V/C = fixarg/vararg/C func

OPADDescription
FUNCFrbaseFixed-arg Lua function
IFUNCFrbaseFixed-arg Lua function, force interpreter
JFUNCFrbaselitFixed-arg Lua function, JIT-compiled
FUNCVrbaseVararg Lua function
IFUNCVrbaseVararg Lua function, force interpreter
JFUNCVrbaselitVararg Lua function, JIT-compiled
FUNCCrbasePseudo-header for C functions
FUNCCWrbasePseudo-header for wrapped C functions
FUNC*rbasePseudo-header for fast functions

函数的热调用也是在这一块触发的,还有这一部分的作用主要是配合call指令,改变var stack的结构,为callee VM函数构建VM运行时栈,如图所示:

在这里插入图片描述

对上面几条指令作出简单解释。

  • 操作数 A 存放函数的帧大小。操作数 D 存放 JFUNCF 和 JFUNCV 指令的追踪编号。
  • 对于 Lua 函数,省略的固定参数会被设为 nil,多余的参数则会被忽略。设置可变参数函数时,会创建一个特殊的 vararg 帧来保存超出固定参数之外的参数;固定参数会被复制到常规 Lua 函数帧中,而它们在 vararg 帧中的槽位则被置为 nil。
  • FUNCF 和 FUNCV 指令用于设置固定参数或可变参数 Lua 函数的帧,并执行热点检测。如果函数被执行的次数足够多,将会触发追踪记录。
  • IFUNCF 和 IFUNCV 指令供 JIT 编译器使用,用于将无法编译的函数加入黑名单。它们不会进行热点检测,而是强制在解释器中执行。
  • JFUNCF 和 JFUNCV 指令在初始设置完成后,会进入 JIT 编译的追踪。
  • FUNCC 和 FUNCCW 指令是由 C 闭包的 pc 字段指向的伪头指令,它们从不会被实际生成,仅用于分派到 C 函数调用的设置代码。
  • 所有编号较高的字节码指令都作为快速函数的伪头指令使用,它们同样从不被实际生成,仅用于分派到相应快速函数的机器码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yelvens

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值