Lua在运行程序之前,首先将它们编译成虚拟机指令(opcodes),然后再去执行这些指令。Lua编译每个函数,为每个函数都创建了一个原型(prototype),原型中的内容有:函数的指令(opcodes)数组,和另一个记录函数中所用到的值(值(TObjects)以及所有常量(字符串、数字))的数组。



    在最初的十年中(自1993年Lua第一次发布),各个版本的Lua使用了基于栈的虚拟机。直到2003年,发布了Lua 5.0,。Lua 5.0使用了基于寄存器的虚拟机。基于寄存器的虚拟机分配活动记录时,也使用栈,寄存器也存在在这个栈上。当Lua进入一个函数,它从栈上预分配一个活动记录。这活动记录要足够大,可以包括函数的全部寄存器。所有局部变量都分配在寄存器中。因此,访问局部变量的效率非常高。



这几个指令操作栈中的数据。在Lua中这几个指令对性能的消耗很高,因为它们涉及到了拷贝带标记的值,见第三章的讨论。因此,寄存器架构有两大优势:避免了大量拷贝,减少了每个函数中指令的数量。Davis等人反对基于寄存器的虚拟机,并提供利用java字节码的方式实现数据。一些作者也反对基于寄存器的虚拟机,因为他们更习惯于一种更快速的编译方式。(例如[24])



    基于寄存器的虚拟机要解决两个问题:代码的大小和解码的速度。在基于寄存器的机器中,指令需要指定它们的操作数,这指令通常比基于栈的指令要大。(例如,Lua虚拟机中的一条指令大小为4b,常见的基于栈的虚拟机(包括以前版本的Lua)的一条指令的大小为1b或者2b)换句话说,基于寄存器的虚拟机减少了指令的数量,增加了指令的大小,所以总体上说代码大小不会增加很大。



    很多指令在栈虚拟机上有隐式的操作数。相应的指令在寄存器虚拟机上,必须将操作数从指令中解码出来。这样的解码增加了解析器的负担。有几个因素可以缓解这样的负担:第一,栈虚拟机也花很多时间在操作隐式的操作数上面(例如增加或减少栈顶)。第二,因为寄存器虚拟机所有的操作数都在指令中,并且指令是一个机器字,解码过程只涉及到一些开销很低的操作,比如逻辑操作。此外,栈虚拟机中的指令经常需要多自己的操作数。例如java的VM,goto和branch指令,要用两字节的指令来替代。由于对齐的原因,解析器不能马上获得这些操作数(至少在可移植的代码中不行,在可移植的代码中都有严格的按照最坏的情况对齐)。在寄存器虚拟机上,因为所有的操作数都在指令中,解析器不需要单独的去获取它们。



    Lua虚拟器一共有35条指令。大部分指令和语言结构相对应:算术运算、table创建和索引,函数和方法的调用,赋值和获取变量的值。还有一个跳转指令的集合,方便的实现了控制结构。图5显示了一个完整的指令集合,以及一些对指令的简介,使用下面的符合表示:R(x)表示第x个寄存器。K(X)表示第x个常量。RK(X)表示R(x)或者K(x-k),这取决于X的值——如果x小于k,就选择R(X)(k是一个内置参数,通常是250)。G[x]表示全局table中的X域。U[x]表示第x个upvalue。查看[14,22]关于Lua虚拟机指令的详细讨论情况。


寄存器保存在运行时的栈中,它(栈)的本质就是就是一个数组。因此访问寄存器非常快。常量和upvalue都存储在数组中,访问它们也很快。全局table是一张普通的Lua表。通过哈希访问它,但是速度也很快,因为它的索引只有字符串(相应的变量名),并且字符串的哈希值都是预计算的,参考第二章。


Move

A B

R(A) := R(B) 

LOADK

A Bx

R(A) := K(Bx) 

LOADBOOL

A B C

R(A) := (Bool)B; if(C)PC ++

LOADNIL

A B

R(A) := ... := R(B) := nil 

GETUPVAL

A B

R(A) := U[B] 

GETGLOBAL

A Bx

R(A) := G[K(Bx)] 

GETTABLE

A B C

R(A) := R(B)[RK(C)] 

SETGLOBAL

A Bx

G[K(Bx)] := R(A)

SETUPVAL

A B

U[B] := R(A)

SETTABLE

A B C

R(A)[RK(B)] := RK(C)

NEWTABLE

A B C

R(A) := {} (size = B, C)

SELF

A B C

R(A+1) := R(B); R(A) := R(B)[RK(C)] 

ADD

A B C

R(A) := RK(B) + RK(C)

SUB

A B C

R(A) := RK(B) - RK(C)

MUL

A B C

R(A) := RK(B) * RK(C)

DIV

A B C

R(A) := RK(B) / RK(C)

POW

A B C

R(A) := RK(B) ^ RK(C)

UNM

A B

R(A) := -R(B) 

NOT

A B

R(A) := not R(B)

CONCAT

A B C

R(A) := R(B) .. ... .. R(C) 

JMP

sBx

PC += sBX --指令寄存器PC

EQ

A B C

if((RK(B) == RK(C)) ~= A) then PC++ 

LT

A B C

if((RK(B) < RK(C)) ~= A) then PC++ 

LE

A B C

if((RK(B) <= RK(C)) ~= A) then PC++ 

TEST

A B C

if(R(B) <=> C) then R(A) := R(B) else PC ++

CALL

A B C

R(A), ..., R(A+C-2) := R(A)(R(A+1), ..., R(A+B-1)) 

TAILCALL

A B C

return R(A)(R(A+!), ..., R(A+B-1)) 

RETURN

A B

return R(A)(R(A+1), ..., R(A+B-1))

PORLOOP

A sBx

R(A) += R(A+2); if R(A) <?= R(A+1) then PC += sBx

TFORLOOP

A C

R(A+2), ..., R(A+2+C) := R(A)(R(A+1), R(A+2));

TFORPREP

A sBx

if type(R(A)) == table then R(A+1) := R(A), R(A) := next;

SETLISTO

A Bx

R(A)[Bx-Bx%FPF+i] := R(A+i), 1 <= i <= Bx%FPF+1

SETLISTO

A Bx

 

CLOSE

A

Close stack variables up to R(A)

CLOSURE

A Bx

R(A) := closure(KPROTO[Bx], R(A), ..., R(A+n))

图5 Lua虚拟机中的指令实现




    Lua虚拟机中的指令将32位分成三或四个域,如图6所示。OP域表示指令占用了6位。其他的域表示操作数。A域总是占用了8位。B和C域分别占9位,它们可以组成18位的域:Bx(unsigned)和sBx(signed)。



    大部分指令使用了三地址的格式:A指向存储结果的寄存器,B和C指向操作数,操作数存储在寄存器或常量中(使用上面提到的RK(X)来表示)。使用这种格式,Lua的几种常用操作都可以编码成单个指令。例如,增加一个局部变量,如a = a + 1,编码成为: ADD x, x, y,x表示寄存器中保存的局部变量,y表示常量1。对于一个赋值表达:a = b.f,a和b都是局部变量,指令也会编码成GETTABLE x y z,x表示寄存器a,y表示寄存器b,z是字符串常量"f"的索引。(在Lua中b.f是b["f"]的一种表达方式,字符串"f"是b的索引)




jvm虚拟机和lua虚拟机区别 lua虚拟机实现_Lua



    分支指令的实现有点困难,因为它需要指定两个操作数和一个跳转偏移量。如果把这些数据都放到一个指令中,会限制跳转偏移最多只能到256(假设用9位的是跳转偏移)。Lua提供了一个解决方案是这样定义的:一条测试指令当测试失败的时候,简单的跳过下一条指令,下一条指令是一条jump指令,它使用最后的18位存储跳转偏移。实际上,测试指令的下一条指令总是jump指令,解释器同时执行这两条指令。当测试指令返回成功时,解析器立即抓取下一条指令并实施跳转操作,而不会等到下一个分派周期去执行jump指令。图7所示一个lua的例子,有代码和相应的字节码。



    图8所示,Lua编译器实施编译优化的一个小例子。图9所示,在使用栈虚拟机的Lua 4.0中编译同样的代码,生成49条指令。要注意的是,寄存器虚拟机能够产生更短的代码。例子中每条可执行的语句,在Lua 5.0中只用一条指令表示,但是在Lua 4.0中要三四条指令表示。



jvm虚拟机和lua虚拟机区别 lua虚拟机实现_jvm虚拟机和lua虚拟机区别_02



    Lua使用了寄存器窗口(register window)来调用函数。Lua计算出调用实参的值,找到第一个未使用的寄存器开始,依此将实参值放入寄存器中。当它执行call时,这些寄存器就变成被调用函数活动记录的一部分了,因此函数可以像通常的局部变量一样访问参数。当函数返回的时候,这些寄存器被放回到调用者的活动记录中去。



    Lua使用两个并行的栈来调用函数。(实际上,每个协程都有属于它们的一对栈,我们在第六章讨论过。)对每个活动的函数而言,一个栈有一个入口。这个入口存放着:被调用的函数、返回地址,和一个指向函数活动记录的基地址。另一个栈是一个简单的大数组,保存这活动记录的Lua值。每个活动记录保存函数所有的临时变量(参数,局部变量,等等)。实际上,在第二个栈中每个项,在第一个栈中都有一个相对应的可变大小的项。(译者注:不知道在说什么)