Lua 代码是如何跑起来的

上一篇「C 代码是如何跑起来的」中,咱们了解了 C 语言这种高级语言是怎么运行起来的。程序员

C 语言虽然也是高级语言,可是毕竟是很 “古老” 的语言了(快 50 岁了)。相比较而言,C 语言的抽象层次并不算高,从 C 语言的表达能力里,仍是能够体会到硬件的影子。segmentfault

旁白:一般而言,抽象层次越高,意味着程序员的在编写代码的时候,心智负担就越小。

今天咱们来看下 Lua 这门相对小众的语言,是如何跑起来的。数组

解释型

不一样于 C 代码,编译器将其直接编译为物理 CPU 能够执行的机器指令,CPU 执行这些机器执行就行。函数

Lua 代码则须要分为两个阶段:优化

  1. 先编译为字节码
  2. Lua 虚拟机解释执行这些字节码
旁白:虽然咱们也能够直接把 Lua 源码做为输入,直接获得执行输出结果,可是实际上内部仍是会分别执行这两个阶段

字节码

「CPU 提供了什么」 中,咱们介绍了物理 CPU 的两大基础能力:提供一系列寄存器,能执行约定的指令集。编码

那么相似的,Lua 虚拟机,也一样提供这两大基础能力:lua

  1. 虚拟寄存器
  2. 执行字节码
旁白: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

稍微解释一下:code

  1. 不像 CPU 提供的物理集群器,有不一样的名字,字节码的虚拟寄存器,是没有名字的,只有数字编号。逻辑上而言,每一个函数有独立的寄存器,都是从序号 0 开始的(实际上会有部分的重叠复用)
  2. Lua 字节码,也提供了定义函数,执行函数的能力
  3. 以上的输出结果是方便人类阅读的格式,实际上字节码是以很是紧凑的二进制来编码的(每一个字节码,定长 32 比特)

执行字节码

Lua 虚拟机

Lua 虚拟机是一个由 C 语言实现的程序,输入是 Lua 字节码,输出是执行这些字节码的结果。内存

对于字节码中的一些抽象,则是在 Lua 虚拟机中来具体实现的,好比:

  1. 虚拟寄存器
  2. 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 寄存器。

函数

几个基本点:

  1. Lua 函数,能够简单的理解为一堆字节码的集合。
  2. Lua 虚拟机里,也有栈帧的,每一个栈帧实际就是一个 C struct 描述的内存结构体。

执行一个 Lua 函数,也就是执行其对应的字节码。

总结

Lua 这种带虚拟机的语言,逻辑上跟物理 CPU 是很相似的。生成字节码,而后由虚拟机来具体执行字节码。

只是多了一层抽象虚拟,字节码解释执行的效率,是比不过机器指令的。

物理内存的读写速度,比物理寄存器要慢几倍甚至几百倍(取决因而否命中 CPU cache)。
因此 Lua 的虚拟寄存器读写,也是比真实寄存器读写要慢不少的。

不过在 Lua 语言的另外一个实现 LuaJIT 中,这种抽象仍是有很大机会来优化的,核心思路跟咱们以前在 「C 代码是如何跑起来的」 中看到的 gcc 的编译优化同样,尽可能多的使用寄存器,减小物理内存的读写。

关于 LuaJIT 确实有不少很牛的地方,之后咱们再分享。