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 确实有不少很牛的地方,之后咱们再分享。

相关文章
相关标签/搜索