您的位置:首页 > 编程语言 > Lua

《Lua 5.0的实现》第七章 - 虚拟机

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

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

    “基于寄存器的代码”避免了几个”push“和”pop“指令,而通常“基于栈的代码”用这几个指令操作栈中的数据。在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表。通过哈希访问它,但是速度也很快,因为它的索引只有字符串(相应的变量名),并且字符串的哈希值都是预计算的,参考第二章。

MoveA BR(A) := R(B) 
LOADKA BxR(A) := K(Bx) 
LOADBOOLA B CR(A) := (Bool)B; if(C)PC ++
LOADNILA BR(A) := ... := R(B) := nil 
GETUPVALA BR(A) := U[B] 
GETGLOBALA BxR(A) := G[K(Bx)] 
GETTABLEA B CR(A) := R(B)[RK(C)] 
SETGLOBALA BxG[K(Bx)] := R(A)
SETUPVALA BU[B] := R(A)
SETTABLEA B CR(A)[RK(B)] := RK(C)
NEWTABLEA B CR(A) := {} (size = B, C)
SELFA B CR(A+1) := R(B); R(A) := R(B)[RK(C)] 
ADDA B CR(A) := RK(B) + RK(C)
SUBA B CR(A) := RK(B) - RK(C)
MULA B CR(A) := RK(B) * RK(C)
DIVA B CR(A) := RK(B) / RK(C)
POWA B CR(A) := RK(B) ^ RK(C)
UNMA BR(A) := -R(B) 
NOTA BR(A) := not R(B)
CONCATA B CR(A) := R(B) .. ... .. R(C) 
JMPsBxPC += sBX --指令寄存器PC
EQA B Cif((RK(B) == RK(C)) ~= A) then PC++ 
LTA B Cif((RK(B) < RK(C)) ~= A) then PC++ 
LEA B Cif((RK(B) <= RK(C)) ~= A) then PC++ 
TESTA B Cif(R(B) <=> C) then R(A) := R(B) else PC ++
CALLA B CR(A), ..., R(A+C-2) := R(A)(R(A+1), ..., R(A+B-1)) 
TAILCALLA B Creturn R(A)(R(A+!), ..., R(A+B-1)) 
RETURNA Breturn R(A)(R(A+1), ..., R(A+B-1))
PORLOOPA sBxR(A) += R(A+2); if R(A) <?= R(A+1) then PC += sBx
TFORLOOPA CR(A+2), ..., R(A+2+C) := R(A)(R(A+1), R(A+2));
TFORPREPA sBxif type(R(A)) == table then R(A+1) := R(A), R(A) := next;
SETLISTOA BxR(A)[Bx-Bx%FPF+i] := R(A+i), 1 <= i <= Bx%FPF+1
SETLISTOA Bx 
CLOSEAClose stack variables up to R(A)
CLOSUREA BxR(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的索引)



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

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



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

    Lua使用两个并行的栈来调用函数。(实际上,每个协程都有属于它们的一对栈,我们在第六章讨论过。)对每个活动的函数而言,一个栈有一个入口。这个入口存放着:被调用的函数、返回地址,和一个指向函数活动记录的基地址。另一个栈是一个简单的大数组,保存这活动记录的Lua值。每个活动记录保存函数所有的临时变量(参数,局部变量,等等)。实际上,在第二个栈中每个项,在第一个栈中都有一个相对应的可变大小的项。(译者注:不知道在说什么)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: