【翻译3/3】The Implementation of Lua 5.0

7 虚拟机

  Lua 通过首先将程序编译为虚拟机的指令(“操作码”)然后执行这些指令来运行程序。 对于 Lua 编译的每个函数,它都会创建一个原型,其中包含一个包含函数操作码的数组和一个包含函数使用的所有常量(文字字符串和数字)的 Lua 值 (TObjects) 数组。
  十年来(自 1993 年 Lua 首次发布以来),Lua 使用了一个基于堆栈的虚拟机,以各种形式出现。 从 2003 年开始,随着 Lua 5.0 的发布,Lua 使用了基于寄存器的虚拟机。 这种基于寄存器的虚拟机还使用堆栈,用于分配寄存器所在的活动记录。 当 Lua 进入一个函数时,它会从堆栈中预分配一个足够大的活动记录,以容纳所有函数寄存器。 所有局部变量都分配在寄存器中。 因此,访问局部变量特别高效。
  基于寄存器的代码避免了基于堆栈的代码需要在堆栈中移动值的几个“push”和“pop”指令。 这些指令在 Lua 中的开销特别大,因为它们涉及标记值的复制,如第 3 节所述。因此,寄存器架构既避免了过多的值复制,又减少了每个函数的指令总数。 戴维斯等人为基于寄存器的虚拟机辩护并提供有关 Java 字节码改进的硬数据。 一些作者还根据基于寄存器的虚拟机对动态编译的适用性进行辩护。
  基于寄存器的机器通常有两个问题:代码大小和解码开销。寄存器机中的指令需要指定其操作数,因此它通常比堆栈机中的相应指令大。 (比如 Lua 的虚拟机中一条指令的大小是 4 个字节,而几个典型的堆栈机器,包括 Lua 以前使用的那些,一条指令的大小是一个或两个字节。)另一方面,寄存器机器生成的操作码比堆栈机器少,所以总代码大小不会大很多。
  堆栈机中的大多数指令都有隐式操作数。寄存器机中的相应指令必须从指令中解码它们的操作数。这种解码会增加解释器的开销。有几个因素可以改善这种开销。首先,堆栈机器还花一些时间来操作隐式操作数(例如,增加或减少栈顶)。其次,因为在寄存器机中所有操作数都在指令内部并且指令是机器字,所以操作数解码只涉及廉价操作,例如逻辑操作。此外,堆栈机器中的指令经常需要多字节操作数。例如,在 Java VM 中,goto 和 branch 指令使用两字节位移。由于对齐,解释器不能立即获取这样的操作数(至少不是使用可移植代码,它必须始终假设最坏情况的对齐限制)。在寄存器机上,由于操作数在指令内部,解释器不必独立获取它们。
  Lua 的虚拟机中有 35 条指令。 大多数指令被选择直接对应于语言结构:算术、表创建和索引、函数和方法调用、设置变量和获取值。 还有一组常规的跳转指令来实现控制结构。 图 5 显示了完整的指令集,并简要总结了每条指令的作用,使用以下符号:R(X) 表示第 X 个寄存器。 K(X) 表示第 X 个常数。 RK(X) 表示 R(X) 或 K(X-k),具体取决于 X 的值——对于小于 k(构建参数,通常为 250)的 X 值,它是 R(X)。 G[X] 表示全局变量表中的字段 X。 U[X] 表示第 X 个上值。 有关 Lua 虚拟机指令的详细讨论。
  寄存器保存在运行时堆栈中,它本质上是一个数组。 因此,对寄存器的访问速度很快。 常量和上值存储在数组中,因此访问它们也很快。 globals 表是一个普通的 Lua 表。 它通过散列访问但具有良好的性能,因为它仅使用字符串(对应于变量名称)进行索引,并且字符串预先计算它们的散列值,如第 2 节所述。


figure5.png

  Lua 虚拟机中的指令占用 32 位,分为三个或四个字段,如图 6 所示。 OP 字段标识指令,占用 6 位。 其他字段代表操作数。 字段 A 始终存在并占用 8 位。 字段 B 和 C 各占 9 位。 它们可以组合成一个 18 位字段:Bx(无符号)和 sBx(有符号)。
  大多数指令使用三地址格式,其中 A 指向保存结果的寄存器,B 和 C 指向操作数,操作数可以是寄存器或常量(使用上面解释的表示形式 RK(X))。 使用这种格式,Lua 中的几个典型操作可以在一条指令中编码。 例如,局部变量的增量,例如 a = a + 1,被编码为 ADD xxy,其中 x 表示保存局部变量的寄存器,y 表示常量 1。像 a = bf 这样的赋值,当两个 a 和 b 是局部变量,也被编码为单个指令 GETTABLE xyz,其中 x 是 a 的寄存器,y 是 b 的寄存器,z 是字符串常量“f”的索引。 (在 Lua 中,语法 b.f 是 b["f"] 的语法糖,即 b 由字符串 "f" 索引。)


figure6.png

figure7.png

  分支指令带来了困难,因为它们需要指定两个要比较的操作数加上一个跳转偏移量。 将所有这些数据打包在一条指令中会将跳转偏移量限制为 256(假设一个有符号的 9 位字段)。 Lua 中采用的解决方案是,从概念上讲,测试指令只是在测试失败时跳过下一条指令; 下一条指令是常规跳转,它使用 18 位偏移量。 实际上,因为一条测试指令后面总是跟着一条跳转指令,所以解释器会一起执行这两条指令。 也就是说,当执行一条成功的测试指令时,解释器立即取出下一条指令并进行跳转,而不是在下一个分派周期中进行。 图 7 显示了 Lua 代码和相应字节码的示例。 注意刚才描述的条件和跳转指令的结构。
figure8.png

figure9.png

  图 8 显示了 Lua 编译器执行的优化的一个小示例。图 9 显示了为 Lua 4.0 编译的相同代码,它使用了具有 49 条指令的基于堆栈的虚拟机。请注意切换到基于寄存器的虚拟机如何允许生成更短的代码。本例中的每个可执行语句在 Lua 5.0 中编译为一条指令,但在 Lua 4.0 中需要三到四条指令。
  对于函数调用,Lua 使用了一种寄存器窗口。它评估连续寄存器中的调用参数,从第一个未使用的寄存器开始。当它执行调用时,这些寄存器成为被调用函数的活动记录的一部分,因此可以像常规局部变量一样访问其参数。当这个函数返回时,这些寄存器被放回到调用者的活动记录中。
  Lua 使用两个并行堆栈进行函数调用。 (实际上,每个协程都有自己的一对堆栈,正如我们在第 6 节中所讨论的。)一个堆栈为每个活动函数都有一个条目。该条目存储被调用的函数、函数调用时的返回地址以及指向函数活动记录的基址索引。另一个堆栈只是一个保存这些活动记录的 Lua 值的大数组。每个活动记录保存函数的所有临时值(参数、局部变量等)。实际上,我们可以将第二个堆栈中的每个条目视为第一个堆栈中相应条目的可变大小部分。

8 结论

  在本文中,我们介绍了 Lua 5.0 实现中最具创新性的方面:基于寄存器的虚拟机、优化用作数组的table的新算法以及闭包的实现。
  据我们所知,Lua 是第一种广泛使用的采用基于寄存器的虚拟机的语言。表的优化允许在以这种方式使用时(即,当它在 1 . . n 范围内具有足够的键时)将table部分实现为数组。它的闭包实现也是独一无二的,将基于数组的堆栈与词法作用域的第一类值函数结合使用,无需复杂的控制流分析。
  图 10 中的表格显示了旧实现和新实现之间的一些简单性能比较。测试在运行 Linux 2.6 的 Intel Pentium IV 机器上运行,该机器有 512 MB,使用 gcc 3.3 编译 Lua。 Lua 4.0 既不使用基于寄存器的虚拟机(它的机器是基于堆栈的)也不使用表-数组优化。 Lua 5’是Lua 5.0,没有表——数组优化、尾调用和动态栈(与协程相关); Lua 5’本质上是带有新的基于寄存器的虚拟机的 Lua 4.0。
  我们采用了 The Great Computer Language Shootout [2] 中的所有测试用例,除了第一个(sum),它是一个简单的循环,用于添加从 1 到 n 的所有整数。 第一个测试大部分时间都在虚拟机中进行; 它表明新虚拟机的速度可以是旧虚拟机的两倍多。 其他测试在其他任务(函数调用、表/数组访问等)上花费的时间更多,因此虚拟机中的增益对总时间的影响较小。 在使用数组(sieve、heapsort、matrix)的测试中,新的虚拟机与新的数组优化相结合,最多可以减少40%的运行时间。
  Lua 5.0 的完整代码可以在 Lua 的网站上浏览:http://www.lua.org/source/5.0/

figure10.png

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容

  • 1 简介   Lua 诞生于一个学术实验室,作为内部软件开发的工具,但不知何故被世界各地的几个工业项目采用且现已广...
    丢番图阅读 573评论 0 0
  • 3 值表示   Lua 是一种动态类型语言:类型附加到值而不是变量。 Lua 有八种基本类型:nil、boolea...
    丢番图阅读 370评论 0 0
  • Lua是用标准C语言编写,是一门以高效率著称的脚本语言。 为了达到较高的执行效率,lua代码并不是直接被Lua解释...
    Jey阅读 1,572评论 0 0
  • Lua代码的执行流程 脚本语言通常都是解释执行的,每一门脚本语言都会有自己定义的OpCode(operation ...
    JunChow520阅读 5,290评论 2 7
  • 前言 编译原理相关的书籍资料五花八门,大多偏理论为主,实用性高的寥寥无几;而讲实践的书,相关的理论太少,难以提炼出...
    丶legend阅读 1,103评论 0 0