上一篇「Lua 代码是如何跑起来的」 中,咱们介绍了标准 Lua 虚拟机是如何运行 Lua 代码的。c++
今天咱们介绍 Lua 语言的另一个虚拟机实现 LuaJIT,LuaJIT 使用的 lua 5.1 的语言标准(也能够兼容 lua 5.2)。意味着一样一份遵照 lua 5.1 标准的代码,既能够用标准 lua 虚拟机来跑,也能够用 LuaJIT 来跑。segmentfault
LuaJIT 主打高性能,接下来咱们看看 LuaJIT 是如何提升性能的。函数
首先,LuaJIT 有两种运行模式,一种是解释模式,这个跟标准 Lua 虚拟机是相似的,不过也有改进的地方。性能
首先,跟标准 Lua 虚拟机同样,Lua 源代码是被编译为字节码(byte code),而后一个个的解释执行这些字节码。
可是,编译出来的字节码,并非跟标准 Lua 同样,只是相似。
模式上来讲,LuaJIT 也是基于虚拟寄存器的,虽然具体实现方式上有所区别。优化
从 Lua 源码到字节码,其实差别不大,可是解释执行字节码,LuaJIT 的改进动做就比较大了。lua
Lua 解释执行字节码,是在 luaV_execute
这个 C 函数里实现的,而 LuaJIT 则是经过手写汇编来实现的。
一般,咱们会简单的认为手写汇编就会更高效,不过也得看写代码的质量。设计
此次咱们经过实际对比双方最终生成的机器码,体验下手写的汇编是如何作到高效的。code
咱们对比「字节码解析」这部分的实现。
首先,Lua 和 LuaJIT 的字节码,都是 32 位定长的。字节码解析的基本逻辑便是:
从虚拟机内部维护的 PC 寄存器,读取 32 位长的字节码,而后解析出 OP 操做码,以及对应的操做参数。get
下面 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 的 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 位能力,能够省掉一些位操做指令。
Just-In-Time 是 LuaJIT 运行 Lua 代码的另外一种模式,也是 LuaJIT 的性能杀手锏。
主要原理是动态生成更加功效的机器指令,从而提高运行时性能。
这个咱们下一篇再继续...