The Implementation of Lua 5.0 中译
https://blog.codingnow.com/2008/05/the_implementation_of_lua_50.html
Lua 用一种称为upvalue 的结构来实现闭包。对任何外层局部变量的存取间 接地通过upvalue 来进行。upvalue 最初指向栈中变量活跃的地方(图4 左边)。 当离开变量作用域时(超过变量生存期时),变量被复制到upvalue 中(图4 右 边)。由于对变量的存取是通过upvalue 里的指针间接进行的,因此复制动作对 任何存取此变量的代码来说都是没有影响的。与内层函数不同的是,声明该局部 变量的函数直接在堆栈中存取它的局部变量。
通过为每个变量至少创建一个upvalue 并按所需情况进行重复利用,保证了 未决状态(是否超过生存期)的局部变量(pending vars)能够在闭包间正确地 共享。为了保证这种唯一性,Lua 为整个运行栈保存了一个链接着所有正打开着 的upvalue(那些当前正指向栈内局部变量的upvalue)的链表(图4 中未决状态 的局部变量的链表)。当Lua 创建一个新的闭包时,它开始遍历所有的外层局部 变量,对于其中的每一个,若在上述upvalue 链表中找到它,就重用此upvalue, 否则,Lua 将创建一个新的upvalue 并加入链表中。注意,一般情况下这种遍历 过程在探查了少数几个节点后就结束了,因为对于每个被内层函数用到的外层局 部变量来说,该链表至少包含一个与其对应的入口(upvalue)。一旦某个关闭的 upvalue 不再被任何闭包所引用,那么它的存储空间就立刻被回收。
与寄存器式虚拟机相关的两个难题是:指令大小和译码速度。寄存器式虚拟 机的指令需要指明操作数位置,因此通常要比堆栈式虚拟机的同类指令长。(例 如,当前Lua 虚拟机的指令长度是4 字节,而其他许多典型的堆栈式虚拟机的指 令长度只有1-2 字节,包括前一版本的Lua 也是。)另一方面,为基于寄存器的 虚拟机生成的操作码要比堆栈式虚拟机少,因此指令总长度大不了多少。 堆栈式虚拟机的许多指令都有隐含的操作数。而寄存器式虚拟机中对应的指 令需要从其中解码出操作数。解码过程增加了解释器的负担。有几个因素会淡化 这种负面影响,第一,堆栈式虚拟机也花费一些时间处理隐含的操作数(例如, 增减栈指针)。第二,由于寄存器式虚拟机中所有操作数都在指令中,而指令是 一个机器字,对操作数的解码过程只包含一些很廉价的操作,如逻辑运算。另外, 堆栈式虚拟机的指令常常需要多字节操作数。如Java 虚拟机JVM 的跳转和分支 指令用了两字节的偏移量。出于对齐的关系,解释器无法一次获取这样的操作数 (至少对于可移植的代码来说是如此,因为它必须总是假定最坏对齐的限制条 件)。在寄存器式虚拟机上,由于操作数在指令里面,解释器无需单独获取它们。
分支指令的实现有点困难,因为每个分支指令需要给出两个用来做比较的操 作数,还需要一个跳转偏移量。将这些都编码到一条指令中将限制跳转距离在 256 之内(假设用9 位的有符号整数域B 或C 做偏移量)。Lua 采用的解决办法 是这样的:概念上讲,当分支指令的测试条件失败时,只跳过后续一个指令;被 跳过的指令是一个无条件跳转指令,它用18 位的偏移量。实际上,由于一个分 支指令后总是有一个跳转指令,解释器会将这两条指令一起执行。也就是说,当 一个分支指令的测试条件为真时,解释器立即取回出下一条指令并执行跳转,而 不会等到下一个分派周期(while-switch 循环)。图6 显示了一个Lua 源码和字节 码的例子。注意观察上述无条件跳转指令紧随分支跳转指令的情况。