共计 2544 个字符,预计需要花费 7 分钟才能阅读完成。
上一篇「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 的确有很多很牛的中央,当前咱们再分享。