什么是 JIT?

    先看一下 LuaJIT 官方的解释:LuaJIT is a Just-In-Time Compiler for the Lua programming language。

    1、 LuaJIT 的组成

    LuaJIT 的运行时环境包括一个用 手写汇编实现的 Lua 解释器 和一个可以 直接生成机器代码的 JIT 编译器

    2、 工作原理

    • 1、Lua 代码在被执行之前总是会先被转换成 LuaJIT 自己定义的 字节码(Byte Code)。 关于 LuaJIT 字节码的文档,可以参见:(这个文档描述的是 LuaJIT 2.0 的字节码,不过 2.1 里面的变化并不算太大)。

    • 2、一开始的时候,Lua 字节码总是被 LuaJIT 的解释器 解释执行

      • LuaJIT 的解释器会在执行字节码的同时记录一些运行时的统计信息,比如每个 Lua 函数调用入口的实际运行次数,还有每个 Lua 循环的实际执行次数。
      • 当这些次数超过某个预设的阈值时,便认为对应的 Lua 函数入口或者对应的 Lua 循环 足够的“热”,这时便会 触发 JIT 编译器开始工作
    • 3、 JIT 编译器会从 热函数 的入口或者 热循环 的某个位置开始尝试编译对应的 Lua 代码路径。 编译的过程是:

      • 首先,把 LuaJIT 字节码 转换成 LuaJIT 自己定义的 中间码(IR)
      • 然后,再生成针对目标体系结构的 机器码(比如 x86_64 指令组成的机器码)。
    • 4、 如果当前 Lua 代码路径上的所有的操作都可以被 JIT 编译器顺利编译,则这条编译过的代码路径便被称为一个 trace,在物理上对应一个 trace 类型的 GC 对象(即参与 Lua GC 的对象)。

    1、 查看工具和内容解析

    • 1、 你可以通过 ngx-lj-gc-objs 工具看到指定的 Nginx worker 进程里所有 trace 对象的一些基本的统计信息,见 https://github.com/openresty/stapxx#ngx-lj-gc-objs

    • 2、 比如下面这一行 ngx-lj-gc-objs 工具的输出:

      输出内容表明:当前进程内的 LuaJIT VM 里一共有 102 个 trace 类型的 GC 对象,其中最小的 trace 占用 160 个字节,最大的占用 928 个字节,平均大小是 337 字节,而所有 trace 的总大小是 34468 个字节。

    2、 不足之处

    LuaJIT 的 JIT 编译器的实现目前还不完整,有一些基本原语它还无法编译,比如:

    • unpack() 函数
    • string.match() 函数
    • 基于 lua_CFunction 实现的 Lua C 模块
    • FNEW 字节码,等等。

    所以当 JIT 编译器在当前代码路径上遇到了它不支持的操作,便会立即终止当前的 trace 编译过程(这被称为 trace abort),而重新退回到解释器模式。

    JIT 编译器不支持的原语被称为 NYI(Not Yet Implemented)原语。比较完整的 NYI 列表在这篇文档里面:http://wiki.luajit.org/NYI

    3、如何避坑

    • 1、 调整对应的 Lua 代码,避免使用 NYI 原语。
    • 2、 增强 JIT 编译器,让越来越多的 NYI 原语能够被编译。

    对于第 2 种方式,春哥一直在推动公司(CloudFlare)赞助 Mike Pall 的开发工作。 不过有些原语因为本身的代价过高,而永远不会被编译,比如基于经典的 lua_CFunction 方式实现的 Lua C 模块(所以需要尽量通过 LuaJIT 的 FFI 来调用 C)。

    而对于第 1 种方法,我们如何才能知道具体是哪一行 Lua 代码上的哪一个 NYI 原语终止了 trace 编译呢?

    答案很简单。就是使用 LuaJIT 安装自带的 jit.vjit.dump 这两个 Lua 模块。这两个 Lua 模块会打印出 JIT 编译器工作的细节过程。

    4、实例分析

    在 Nginx 的上下文中,我们可以在 nginx.conf 文件中的 http {} 配置块中添加下面这一段:

    1. init_by_lua_block {
    2. local verbose = false
    3. if verbose then
    4. local dump = require("jit.dump")
    5. dump.on(nil, "/tmp/jit.log")
    6. else
    7. local v = require("jit.v")
    8. end
    9. require("resty.core")
    10. }

    那一行 require("resty.core") 倒并不是必需的,放在那里的主要目的是为了尽量避免使用 ngx_lua 模块自己的基于 lua_CFunction 的 Lua API,减少 NYI 原语。

    在上面这段 Lua 代码中,可以下分为如下两种情况:

    • verbose 变量为 false 时(默认就为 false 哈),我们使用 jit.v 模块打印出比较简略的流水信息到 /tmp/jit.log 文件中;
    • 而当 verbose 变量为 true 时,我们则使用 jit.dump 模块打印所有的细节信息,包括每个 trace 内部的字节码、IR 码和最终生成的机器指令。

    这里我们主要以 jit.v 模块为例。 在启动 Nginx 之后,应当使用 abweighttp 这样的工具对相应的服务接口进行预热,以触发 LuaJIT 的 JIT 编译器开始工作(还记得刚才我们说的 “热函数”“热循环” 吗?)。 预热过程一般不用太久,跑个二三百个请求足矣。当然,压更多的请求也没关系。完事后,我们就可以检查 /tmp/jit.log 文件里面的输出了。

    jit.v 模块的输出里如果有类似下面这种带编号的 TRACE 行,则表示成功编译了的 trace 对象,例如:

    • 1、单行的

      1. [TRACE 6 shdict.lua:126 return]

      解析:这个 trace 对象编号为 6,对应的 Lua 代码路径是从 shdict.lua 文件的第 126 行开始的。

    • 2、关联的 下面这样的也是成功编译了的 trace:

      解析:这个 trace 编号为 16,是从 waf-core.lua 文件的第 419 行开始的,同时它和编号为 15 的 trace 联接了起来。

      1. [TRACE --- waf-core.lua:455 -- NYI: FastFunc pairs at waf-core.lua:458]

      解析:上面这一行是说,这个 trace 是从 waf-core.lua 文件的第 455 行开始编译的,但当编译到 waf-core.lua 文件的第 458 行时,遇到了一个 NYI 原语编译不了,即 这个内建函数,于是当前的 trace 编译过程被迫终止了。

      类似的例子还有下面这些:

      1. [TRACE --- waf.lua:321 -- NYI: bytecode 51 at raven.lua:107]

      解析:上面第二行是因为操作码 51 的 LuaJIT 字节码也是 NYI 原语,编译不了。

    5、 探查字节码的工具

    那么我们如何知道 51 字节码究竟是啥呢?我们可以用 nginx-devel-utils 项目中的 ljbc.lua 脚本来取得 51 号字节码的名字:

    我们看到原来是用来(动态)创建 Lua 函数的 FNEW 字节码。

    ljbc.lua 脚本的位置是:

    1. https://github.com/agentzh/nginx-devel-utils/blob/master/ljbc.lua

    非常简单的一个脚本,就几行 Lua 代码。

    这里需要提醒的是,不同版本的 LuaJIT 的字节码可能是不相同的,所以一定要使用和你的 Nginx 链接的同一个 LuaJIT 来运行这个 ljbc.lua 工具,否则有可能会得到错误的结果。

    6、对比实验

    我们实际做个对比实验,看看 JIT 带来的好处:

    1. # cat test.lua
    2. local s = [[aaaaaabbbbbbbcccccccccccddddddddddddeeeeeeeeeeeee
    3. fffffffffffffffffggggggggggggggaaaaaaaaaaabbbbbbbbbbbbbb
    4. ccccccccccclllll]]
    5. for i=1,10000 do
    6. for j=1,10000 do
    7. string.find(s, "ll", 1, true)
    8. end
    9. end
    10. # time luajit test.lua
    11. 5.19s user
    12. 0.03s system
    13. 96% cpu
    14. 5.392 total
    15. # time lua test.lua
    16. 9.20s user
    17. 0.02s system
    18. 99% cpu
    19. 9.270 total

    本例子可以看到效率相差大约 9.2/5.19 ≈ 1.77 倍,换句话说标准 Lua 需要 177% 的时间才能完成同样的工作。估计大家觉得这个还不过瘾,再看下面示例代码:

    从这个执行结果中,大致可以总结出下面几点:

    • 在标准 Lua 解释器中,使用 ipairspairs 没有区别
    • 对于 ipairs 方式,LuaJIT 的性能大约是标准 Lua 的 40 倍

    基础库的支持情况

    字符串库

    math 库