Lua 虚拟机指令

摘要:
Lua虚拟机指令当Lua运行代码时,首先将代码编译为虚拟机指令,然后执行它们。1从Lua5.0开始,Lua使用基于寄存器的虚拟机。但是基于寄存器的虚拟机仍然存在两个问题:指令长度和解码成本。Lua虚拟机有35条指令,其中大多数直接与语言结构交互,如算术、表创建和索引、函数和方法调用、写入和读取变量值。以下代码是Lua中指令名称的定义。Lua虚拟机中的指令长度为32位,分为3~4个域。在这种形式下,Lua中的一些典型操作可以转换为指令。
Lua 虚拟机指令

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个域。其中

    1. 指令的OP域占6位,即最多有64条指令。
    2. A域占8位。
    3. B和C域可以分别占9位,或组合成18位的Bx域(unsigned)或sBx域(signed)。

大部分指令使用三地址格式,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)。

免责声明:文章转载自《Lua 虚拟机指令》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇centos 卸载删除 mysql (mariadb)OSPF 做负载均衡下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

深入Java虚拟机之内存区域与内存溢出

一.内存区域 Java虚拟机在执行Java程序的过程中会把他所管理的内存划分为若干个不同的数据区域。Java虚拟机规范将JVM所管理的内存分为以下几个运行时数据区:程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区。下面详细阐述各数据区所存储的数据类型。 程序计数器(ProgramCounterRegister) 一块较小的内存空间,它是当...

在linux环境下搭建java web测试环境(非常详细!!)

一.项目必备软件及基本思路 项目必备:虚拟机:VMware Workstation (已安装linux的 CentOS6.5版本) 项目:java web项目 (必须在本地部署编译后选择项目的webRoot,改为ROOT(ROOT包含下面四个关键文件),放到tomcat下的webapps下即可,因为tomcat启用一个工程的时候,就是发布了除了JSP以外的...

虚拟机CentOS7三台集群配置网络

准备工作,VMware中安装一台Centos7,然后完全克隆出两个,一共三台虚拟机,下面是对三台虚拟机的网络进行配置    这三台环境一模一样,分别命名为:Master、Node1、Node2,在配置网络之前,三台虚拟机先同时开启,开始集群网络配置: 第一、进入虚拟机网络编辑器,选择VMnet8, 选择NAT模式,确定子网IP段,我的电脑一进来默认是192...

你知道 Java 代码是如何运行的吗?

对于任何一门语言,要想达到精通的水平,研究它的执行原理(或者叫底层机制)不失为一种良好的方式。 在本篇文章中,将重点研究java源代码的执行原理,即从程序员编写JAVA源代码,到最终形成产品,在整个过程中,都经历了什么?每一步又是怎么执行的?执行原理又是什么? 一 编写java源程序 java源文件:指存储java源码的文件。 先来看看如下代码: (1...

理解全虚拟、半虚拟以及硬件辅助的虚拟化

转自: http://blog.csdn.net/flyforfreedom2008/article/details/45113635 接触过的一些搞了几年云计算的童鞋,也没明白常见的几种虚拟机技术方案的异同,比如只是记住了半虚拟要在虚拟机装驱动而全虚拟不需要,也不知道有时候为什么需要打开BIOS里的VT项。本人呢,在看了各种讲解虚拟化的书籍之后,有些概念...

005_MAC下的VMware fushion快捷键(折中)

由于MAC和VMware Fushion虚拟机之间有一些快捷键的映射,所以Windows虚拟机就找了一个折中的方案。现总结MAC下的win常用快捷键==> <1>最小化窗口(Alt + Space + N)。这时采用alt + tab切换应用程序。 如果是想要: 显示桌面 这个效果的话, 开始(键盘上的windows图标那个按键) + D...