5

上一篇「Lua 代码是如何跑起来的」 中,我们介绍了标准 Lua 虚拟机是如何运行 Lua 代码的。

今天我们介绍 Lua 语言的另外一个虚拟机实现 LuaJIT,LuaJIT 使用的 lua 5.1 的语言标准(也可以兼容 lua 5.2)。意味着同样一份遵守 lua 5.1 标准的代码,既可以用标准 lua 虚拟机来跑,也可以用 LuaJIT 来跑。

LuaJIT 主打高性能,接下来我们看看 LuaJIT 是如何提高性能的。

解释模式

首先,LuaJIT 有两种运行模式,一种是解释模式,这个跟标准 Lua 虚拟机是类似的,不过也有改进的地方。

首先,跟标准 Lua 虚拟机一样,Lua 源代码是被编译为字节码(byte code),然后一个个的解释执行这些字节码。
但是,编译出来的字节码,并不是跟标准 Lua 一样,只是类似。
模式上来说,LuaJIT 也是基于虚拟寄存器的,虽然具体实现方式上有所区别。

解释执行字节码

从 Lua 源码到字节码,其实差异不大,但是解释执行字节码,LuaJIT 的改进动作就比较大了。

Lua 解释执行字节码,是在 luaV_execute 这个 C 函数里实现的,而 LuaJIT 则是通过手写汇编来实现的。
通常,我们会简单的认为手写汇编就会更高效,不过也得看写代码的质量。

对比最终生成的机器码

这次我们通过实际对比双方最终生成的机器码,体验下手写的汇编是如何做到高效的。

我们对比「字节码解析」这部分的实现。
首先,Lua 和 LuaJIT 的字节码,都是 32 位定长的。字节码解析的基本逻辑即是:
从虚拟机内部维护的 PC 寄存器,读取 32 位长的字节码,然后解析出 OP 操作码,以及对应的操作参数。

LuaJIT

下面 LuaJIT 源码中的「字节码解析」的源代码,
这里并不是裸写的汇编代码,为了提高可阅读性,用到了一些宏。

mov RCd, [PC]
movzx RAd, RCH
movzx OP, RCL
add PC, 4
shr RCd, 16

最终在 x86_64 上生成的机器指令如下,非常的简洁。

mov    eax,DWORD PTR [rbx]  # rbx 里存储的是 PC 值,读取 32 位字节码到 eax 寄存器
movzx  ecx,ah               # 9-16 位,是操作数 A 的值
movzx  ebp,al               # 低 8 位是 OP 操作码
add    rbx,0x4              # PC 指向下一个字节码
shr    eax,0x10             # 右移 16 位,是操作数 C 的值
Lua

在 Lua 的 luaV_execute 函数中,大致是有这些 C 源代码来完成「字节码解析」的部分工作。

const Instruction i = *pc++;
ra = RA(i);
GET_OPCODE(i)

经过 gcc 编译之后,我们从可执行文件中,可以找到如下相对应的机器指令。
因为 gcc 是对整个函数进行通盘优化,所以指令的顺序并不是那么直观,寄存器使用也不是那么统一,所以看起来会有点乱。
如下是我摘出来的机器指令,为了方便阅读,顺序也经过了调整,没有保持原始的顺序。

mov    ebx,DWORD PTR [r14]   # r14 里存储的是 PC 值,读取 32 位字节码到 ebx 寄存器
lea    r12,[r14+0x4]         # PC 指向下一个字节码,存入 r12
mov    r14,r12               # 后续再复制到 r14(因为 r14 中间还有其他用途)

mov    edx,ebx               # 复制 edx 到 eax
and    edx,0x3f              # 低 6 位是 OP 操作码

# 7-14 位是操作数 A 的值
mov    eax,ebx               # 复制 ebx 到 eax
shr    eax,0x6               # 右移 6 位
movzx  eax,al                # 此时的低 8 位是操作数 A 的值

# 此时对应操作数的使用,不属于字节码解析了,但是是 RA(i) 里的实现
shl    rax,0x4               # rax * 16
lea    r9,[r11+rax*1]        # r11 是 BASE 的值,取操作 A 对应 Lua
 栈上的地址

对比分析

字节码解析,是 Lua 中最基础的操作。
通过对比最终生成的机器码,我们明显可以看到 LuaJIT 的实现可以更加高效。

手写汇编可以更好的利用寄存器,不过,也不完全是因为手写汇编的原因。
LuaJIT 从字节码设计上,就考虑到了高效,OP code 直接是 8 位,这样可以直接利用 al 这种 CPU 硬件提供的低 8 位能力,可以省掉一些位操作指令。

JIT

Just-In-Time 是 LuaJIT 运行 Lua 代码的另一种模式,也是 LuaJIT 的性能杀手锏。
主要原理是动态生成更加功效的机器指令,从而提升运行时性能。

这个我们下一篇再继续...


doujiang24
209 声望1k 粉丝

Core developer of OpenResty.