Lua是用标准C语言编写,是一门以高效率著称的脚本语言。
为了达到较高的执行效率,lua代码并不是直接被Lua解释器解释执行,而是先由Lua编译器编译为字节码,然后再交给Lua虚拟机执行。
二进制chunk?
在Lua的行话里,一段可以被Lua解释器解释执行的Lua代码就叫作chunk,Lua并不是直接解释执行chunk,先由编译器编译成字节码,编译成的字节码就叫二进制chunk,由虚拟机在执行字节码。
Luac命令介
作为编译器,把Lua源文件编译成二进制chunk文件,也可作为反编译器,分析字节码的内容
// 下载lua代码
// 进入到当前文件夹
cd /Users/qs/Desktop/Jey/lua-5.4.3
// 执行make命令,生成两个可执行程序lua和luac
make
// 把那个两个文件拷贝出来,放到一个bin文件夹中,进入bin,执行./lua,就进入了lua命令行
./lua
// 创建一个hallo.lua文件,print("hallo lua")
nano hallo.lua
// 执行,打印出Hallo lua
./lua hallo.lua
// 编译成字节码,会编译成一个luac.out文件
./luac hallo.lua
// 执行下面也是可以的
./luac luac.out
二进制chunk(Binary chunk)的格式
没有标准化,也没有任何官方文档对其进行说明,一切以lua官方实现的源代码为准。
- 其设计并没有考虑跨平台,对于需要超过一个字节表示的数据,必须要考虑大小端(Endianness)问题。
lua官方实现的做法比较简单:编译lua脚本时,直接按照本机的大小端方式生成二进制chunk文件,当加载二进制chunk文件时,会探测被加载文件的大小端方式,如果和本机不匹配,就拒绝加载.
大端序:数据的高位字节存放在地址的低端 低位字节存放在地址高端。
小端序:数据的高位字节存放在地址的高端 低位字节存放在地址低端。
二进制chunk格式设计也没有考虑不同lua版本之间的兼容问题,当加载二进制chunk文件时,会检测其版本号,如果和当前lua版本不匹配,就拒绝加载
另外,二进制chunk格式设计也没有被刻意设计得很紧凑。在某些情况下,一段lua代码编译成二进制chunk后,甚至会被文本形式的源代码还要大。
预编译成二进制chunk主要是为了提升加载速度,因此这也不是很大的问题.
Stack Based VM vs Rigister Based VM
高级编程语言的虚拟机是利用软件技术对硬件进行的模拟和抽象。按照实现方式,可分为两类:基于栈(Stack Based)和基于寄存器(Rigister Based)。
Lua从1.0版本就开始内置了虚拟机,到5.0之前的版本都是基于栈,5.0之后是基于寄存器。
a. 基于栈的虚拟机,会转化成如下指令
push b; // 将变量b的值压入stack
push c; // 将变量c的值压入stack
add; // 将stack顶部的两个值弹出后相加,然后将结果压入stack顶
mov a; // 将stack顶部结果放到a中
所有的指令执行,都是基于一个操作数栈的。你想要执行任何指令时,对不起,得先入栈,然后算完了再给我出栈。
由于 Stack Based VM 的指令都是基于当前 stack 来查找操作数的,这就相当于所有操作数的存储位置都是运行期决定的,在编译器的代码生成阶段不需要额外为在哪里存储操作数费心,所以 stack based 的编译器实现起来相对比较简单直接,也正因为这个原因,每条指令占用的存储空间也比较小。
但是,对于一个简单的运算,stack based vm 会使用过多的指令组合来完成,这样就增加了整体指令集合的长度。vm 会使用同样多的迭代次数来执行这些指令,这对于效率来说会有很大的影响。并且,由于操作数都要放到stack上面,使得移动这些操作数的内存复制大大增加,这也会影响到效率。
b. 基于寄存器的虚拟机,会转化成如下指令
add a b c; // 将b与c对应的寄存器的值相加,将结果保存在a对应的寄存器中
没有操作数栈这一概念,但是会有许多的虚拟寄存器。这类虚拟寄存器有别于CPU的寄存器,因为CPU寄存器往往是定址的(比如DX本身就是能存东西),而寄存器式的虚拟机中的寄存器通常有两层含义:
(1)寄存器别名(比如lua里的RA、RB、RC、RBx等),它们往往只是起到一个地址映射的功能,它会根据指令中跟操作数相关的字段计算出操作数实际的内存地址,从而取出操作数进行计算;
(2)实际寄存器,有点类似操作数栈,也是一个全局的运行时栈,只不过这个栈是跟函数走的,一个函数对应一个栈帧,栈帧里每个slot就是一个寄存器,第1步中通过别名映射后的地址就是每个slot的地址。
好处是指令条数少,数据转移次数少。坏处是单挑指令长度较长。
具体来看,lua里的实际寄存器数组是用TValue结构的栈来模拟的,这个栈也是lua和C进行交互的虚拟栈。
基于栈的虚拟机需要使用PUSH类指令往栈顶推入值,使用POP类指令从栈顶弹出值,其他指令则是对栈顶值进行操作,因此指令集相对比较大,但是指令的平均长度比较短;
基于寄存器的虚拟机由于可以直接对寄存器进行寻址,所以不需要PUSH或者POP类指令,指令集相对比较小,但是由于需要把寄存器地址编码进指令里,所以指令的平均长度比较长。
Lua函数原型结构
lua编译器以函数为单位对源代码进行编译,每个函数会被编译成一个称之为原型(Prototype)的结构
原型主要包含6部分内容:函数基本信息(basic info:含参数数量、局部变量数量等信息)、字节码(bytecodes)、常量(constants)表、upvalue(闭包捕获的非局部变量)表、调试信息(debug info)、子函数原型列表(sub functions)