Lua运行代码时,首先把代码编译成虚拟机的指令("opcode"),然后执行它们。 Lua编译器为每个函数创建一个原型(prototype),这个原型包含函数执行的一组指令和函数所用到的数据表。1
从Lua5.0开始,Lua使用基于寄存器的虚拟机(虚拟机主要分为基于寄存器的和基于栈的)。 为了分配寄存器使用时的activation record,这个虚拟机也使用到了栈。 当Lua进入函数时,它从栈中预分配了足够容纳所有函数寄存器的activation record。 所有的局部变量在寄存器中分配。因此提高了访问局部变量的效率。
基于寄存器的指令避免了“push”和“pop”操作,而这正式基于栈的虚拟机需要的。 这些操作在Lua中十分昂贵,因为它们涉及了对值的拷贝。 所以寄存器结构能够避免昂贵的值拷本,以及减少每个函数指令个数。
但基于寄存器的虚拟机仍有两个问题:指令的长度和译码的代价。 一条基于寄存器的指令需要指明它的操作对象,所以它比一般基于栈的指令要长(如Lua的指令为4个字节,而基于栈的指令只需1~2个字节)。 另一方面,基于寄存器虚拟机一般都比基于栈的虚拟机产生更少的操作,因此总体长度不会很长。
大多数基于栈的指令都有隐式操作对象。 而基于寄存器的指令是从指令中取出操作对象,这增加了解释器的开销。 针对这个开销,以下有几点分析。 第一,基于栈的虚拟机仍需要寻找隐藏的操作对象。 第二,有时基于寄存器的操作对象有更低的运算代价,如逻辑操作。 而基于栈的虚拟机一个指令有时需要多次操作。
Lua虚拟机有35条指令,大部分指令与语言结构直接交互,如算术、table创建和索引、函数和方法的调用、写入和读取变量的值。 当然还有一组常规的跳转指令来实现过程控制。 下段代码是Lua中指令名称的定义。
--------------------------------------------------------------------------- // R(x) - register // Kst(x) - constant 常量 (in constant table) // RK(x) == if ISK(x) then Kst(INDEXK(x)) else R(x) typedef enum { /*---------------------------------------------------------------------- name args description ------------------------------------------------------------------------*/ OP_MOVE,/* A B R(A) := R(B) */ OP_LOADK,/* A Bx R(A) := Kst(Bx) */ OP_LOADBOOL,/* A B C R(A) := (Bool)B; if (C) pc++ */ OP_LOADNIL,/* A B R(A) := ... := R(B) := nil */ OP_GETUPVAL,/* A B R(A) := UpValue[B] */ OP_GETGLOBAL,/* A Bx R(A) := Gbl[Kst(Bx)] */ OP_GETTABLE,/* A B C R(A) := R(B)[RK(C)] */ OP_SETGLOBAL,/* A Bx Gbl[Kst(Bx)] := R(A) */ OP_SETUPVAL,/* A B UpValue[B] := R(A) */ OP_SETTABLE,/* A B C R(A)[RK(B)] := RK(C) */ OP_NEWTABLE,/* A B C R(A) := {} (size = B,C) */ OP_SELF,/* A B C R(A+1) := R(B); R(A) := R(B)[RK(C)] */ OP_ADD,/* A B C R(A) := RK(B) + RK(C) */ OP_SUB,/* A B C R(A) := RK(B) - RK(C) */ OP_MUL,/* A B C R(A) := RK(B) * RK(C) */ OP_DIV,/* A B C R(A) := RK(B) / RK(C) */ OP_MOD,/* A B C R(A) := RK(B) % RK(C) */ OP_POW,/* A B C R(A) := RK(B) ^ RK(C) */ OP_UNM,/* A B R(A) := -R(B) */ OP_NOT,/* A B R(A) := not R(B) */ OP_LEN,/* A B R(A) := length of R(B) */ OP_CONCAT,/* A B C R(A) := R(B).. ... ..R(C) */ OP_JMP,/* sBx pc+=sBx */ OP_EQ,/* A B C if ((RK(B) == RK(C)) ~= A) then pc++ */ OP_LT,/* A B C if ((RK(B) < RK(C)) ~= A) then pc++ */ OP_LE,/* A B C if ((RK(B) <= RK(C)) ~= A) then pc++ */ OP_TEST,/* A C if not (R(A) <=> C) then pc++ */ OP_TESTSET,/* A B C if (R(B) <=> C) then R(A) := R(B) else pc++ */ OP_CALL,/* A B C R(A), ... ,R(A+C-2) := R(A)(R(A+1), ... ,R(A+B-1)) */ OP_TAILCALL,/* A B C return R(A)(R(A+1), ... ,R(A+B-1)) */ OP_RETURN,/* A B return R(A), ... ,R(A+B-2) (see note) */ OP_FORLOOP,/* A sBx R(A)+=R(A+2); if R(A) =) R(A)*/ OP_CLOSURE,/* A Bx R(A) := closure(KPROTO[Bx], R(A), ... ,R(A+n)) */ OP_VARARG/* A B R(A), R(A+1), ..., R(A+B-1) = vararg */ } OpCode;---------------------------------------------------------------------------
寄存器保存在运行时栈中,且这个栈也是一个能随机访问的数组,这样能快速访问寄存器。 常量和上值(upvalue)2也保存在数组中,所以访问它们也很快。 全局表是一个传统的Lua table,它通过strings来快速访问,并且strings已经预计算出了它们的hash值。
Lua虚拟机中指令的长度为32位,划分为3~4个域。其中
大部分指令使用三地址格式,A指向保存结果的寄存器,B和C指向操作操作目标,即寄存器或常量。 使用这种形式,一些Lua中典型的操作能够译成一条指令。 如a = a + 1
可以被译成ADD x x y
,其中x代表寄存器保存的变量,y代表常量1。 a = b.f
可以被译成GETTABLE x y z
,其中x代表a的寄存器,y代表b的寄存器,z代表常量f的索引。
转移指令使用4地址格式时会有一些困难,因为这将限制偏移的长度在256内(9-bit field)。 Lua的解决方法是,当测试指令(test instruction)判断为否时,跳过下一条转移指令;否则下一条指令是一个正常跳转指令,使用18位的偏移。 这也就是说测试指令后总跟着一条转移指令,那么解释器可以把这两条指令一起执行。
对于函数调用,Lua使用一种寄存器窗口(register window)的方式。它使用第一个没有使用的寄存器来连续存储参数。 当执行函数调用时,这些寄存器称为调用的函数的activation recode的一部分,使得函数像访问局部变量一样访问这些参数。 当函数返回时,这些寄存器加入到调用函数的上下文的activation recode里。
Lua为函数调用使用两个平行的栈。 一个栈保存着每个激活函数的信息项。这条信息保存着调用函数、返回地址和指向函数activation record的索引。 另一条栈是一条保存了activation records的数组。每一条activation record保存了函数所有的临时变量。 实际上,我们可以把第二条栈中的每个项看成第一个栈中的交互项的变量大小部分。
1. R. Ierusalimschy, L. H. de Figueiredo, W. Celes, The implementation of Lua 5.0, Journal of Universal Computer Science 11 #7 (2005) 1159–1176. [jucs · slides]
2. 内部嵌套函数使用的外部函数的局部变量在函数内部称之为上值(upvalue),或者外局部变量(external local variable)。