上一篇「C 代码是如何跑起来的」中,咱们理解了 C 语言这种高级语言是怎么运行起来的。
C 语言尽管也是高级语言,然而毕竟是很 “古老” 的语言了(快 50 岁了)。相比较而言,C 语言的抽象层次并不算高,从 C 语言的表达能力里,还是能够领会到硬件的影子。
旁白:通常而言,抽象层次越高,意味着程序员的在编写代码的时候,心智累赘就越小。
明天咱们来看下 Lua 这门绝对小众的语言,是如何跑起来的。
解释型
不同于 C 代码,编译器将其间接编译为物理 CPU 能够执行的机器指令,CPU 执行这些机器执行就行。
Lua 代码则须要分为两个阶段:
- 先编译为字节码
- Lua 虚拟机解释执行这些字节码
旁白:尽管咱们也能够间接把 Lua 源码作为输出,间接失去执行输入后果,然而实际上外部还是会别离执行这两个阶段
字节码
在「CPU 提供了什么」 中,咱们介绍了物理 CPU 的两大根底能力:提供一系列寄存器,能执行约定的指令集。
那么相似的,Lua 虚拟机,也同样提供这两大根底能力:
- 虚构寄存器
- 执行字节码
旁白:Lua 寄存器式虚拟机,会提供虚构的寄存器,市面上更多的虚拟机是栈式的,没有提供虚构寄存器,然而会对应的操作数栈。
咱们来用如下一段 Lua 代码(是的,逻辑跟上一篇中的 C 代码一样),看看对应的字节码。用 Lua 5.1.5 中的 luac
编译能够失去如下后果:
$ ./luac -l simple.lua
main <simple.lua:0,0> (12 instructions, 48 bytes at 0x56150cb5a860)
0+ params, 7 slots, 0 upvalues, 4 locals, 4 constants, 1 function
1 [4] CLOSURE 0 0 ; 0x56150cb5aac0
2 [6] LOADK 1 -1 ; 1 # 将常量区中 -1 地位的值(1) 加载到寄存器 1 中
3 [7] LOADK 2 -2 ; 2 # 将常量区中 -2 地位的值(2) 加载到寄存器 1 中
4 [8] MOVE 3 0 # 将寄存器 0 的值,挪到寄存器 3
5 [8] MOVE 4 1
6 [8] MOVE 5 2
7 [8] CALL 3 3 2 # 调用寄存器 3 的函数,寄存器 4,和寄存器 5 作为两个函数参数,返回值放入寄存器 3 中
8 [10] GETGLOBAL 4 -3 ; print
9 [10] LOADK 5 -4 ; "a + b = "
10 [10] MOVE 6 3
11 [10] CALL 4 3 1
12 [10] RETURN 0 1
function <simple.lua:2,4> (3 instructions, 12 bytes at 0x56150cb5aac0)
2 params, 3 slots, 0 upvalues, 2 locals, 0 constants, 0 functions
1 [3] ADD 2 0 1 # 将寄存器 0 和 寄存器 1 的数相加,后果放入寄存器 2 中
2 [3] RETURN 2 2 # 将寄存器 2 中的值,作为返回值
3 [4] RETURN 0 1
略微解释一下:
- 不像 CPU 提供的物理集群器,有不同的名字,字节码的虚构寄存器,是没有名字的,只有数字编号。逻辑上而言,每个函数有独立的寄存器,都是从序号
0
开始的(实际上会有局部的重叠复用) - Lua 字节码,也提供了定义函数,执行函数的能力
- 以上的输入后果是不便人类浏览的格局,实际上字节码是以十分紧凑的二进制来编码的(每个字节码,定长 32 比特)
执行字节码
Lua 虚拟机
Lua 虚拟机是一个由 C 语言实现的程序,输出是 Lua 字节码,输入是执行这些字节码的后果。
对于字节码中的一些形象,则是在 Lua 虚拟机中来具体实现的,比方:
- 虚构寄存器
- Lua 变量,比方
table
等
虚构寄存器
对于字节码中用到的虚构寄存器,Lua 虚拟机是用一段间断的物理内存来模仿。
具体来说:
因为 Lua 变量,在 Lua 虚拟机外部,都是通过 TValue
构造体来存储的,所以实际上虚构寄存器,就是一个 TValue
数组。
例如上面的 MOVE
指令:
MOVE 3 0
实际上是实现一个 TValue
的赋值,这是 Lua 5.1.5 中对应的 C 代码:
#define setobj(L,obj1,obj2) \
{ const TValue *o2=(obj2); TValue *o1=(obj1); \
o1->value = o2->value; o1->tt=o2->tt; \
checkliveness(G(L),o1); }
其对应的要害机器指令如下:(次要是通过 mov
机器指令来实现内存的读写)
0x00005555555686f1 <+1889>: mov rdx,QWORD PTR [rax]
0x00005555555686f4 <+1892>: mov r14,r12
0x00005555555686f7 <+1895>: mov QWORD PTR [r9],rdx
0x00005555555686fa <+1898>: mov eax,DWORD PTR [rax+0x8]
0x00005555555686fd <+1901>: mov DWORD PTR [r9+0x8],eax
执行
Lua 虚拟机的实现中,有这样一个 for (;;)
有限循环(在 luaV_execute
函数中)。
其外围工作跟物理 CPU 相似,读取 pc
地址的字节码(同时 pc
地址 +1
),解析操作指令,而后依据操作指令,以及对应的操作数,执行字节码。
例如下面咱们解释过的 MOVE
字节码指令,也就是在这个循环中执行的。其余的字节码指令,也是相似的套路来实现执行的。
pc
指针也只是一个 Lua 虚拟机地位的内存地址,并不是物理 CPU 中的 pc
寄存器。
函数
几个基本点:
- Lua 函数,能够简略的了解为一堆字节码的汇合。
- Lua 虚拟机里,也有栈帧的,每个栈帧理论就是一个 C struct 形容的内存构造体。
执行一个 Lua 函数,也就是执行其对应的字节码。
总结
Lua 这种带虚拟机的语言,逻辑上跟物理 CPU 是很相似的。生成字节码,而后由虚拟机来具体执行字节码。
只是多了一层形象虚构,字节码解释执行的效率,是比不过机器指令的。
物理内存的读写速度,比物理寄存器要慢几倍甚至几百倍(取决于是否命中 CPU cache)。
所以 Lua 的虚构寄存器读写,也是比实在寄存器读写要慢很多的。
不过在 Lua 语言的另一个实现 LuaJIT 中,这种形象还是有很大机会来优化的,外围思路跟咱们之前在 「C 代码是如何跑起来的」 中看到的 gcc
的编译优化一样,尽量多的应用寄存器,缩小物理内存的读写。
对于 LuaJIT 的确有很多很牛的中央,当前咱们再分享。
发表回复