🔬
OpenResty 最佳实践
  • 序
  • 入门篇
  • Lua 入门
    • Lua 简介
    • Lua 环境搭建
    • Lua 编辑器选择
    • 基础数据类型
    • 表达式
    • 控制结构
      • if/else
      • while
      • repeat
      • for
      • break,return 和 goto
    • Lua 函数
      • 函数的定义
      • 函数的参数
      • 函数返回值
      • 全动态函数调用
    • 模块
    • String 库
    • Table 库
    • 日期时间函数
    • 数学库函数
    • 文件操作
  • Lua 高阶
    • 元表
    • 面向对象编程
    • 局部变量
    • 判断数组大小
    • 非空判断
    • 正则表达式
    • 虚变量
    • 抵制使用 module() 定义模块
    • 调用代码前先定义函数
    • 点号与冒号操作符的区别
    • module 是邪恶的
    • FFI
    • 什么是 JIT
  • Nginx
    • Nginx 新手起步
    • location 匹配规则
    • 静态文件服务
    • 日志
    • 反向代理
    • 负载均衡
    • 陷阱和常见错误
  • OpenResty
    • 环境搭建
      • Windows 平台
      • CentOS 平台
      • Ubuntu 平台
      • Mac OS X 平台
    • Hello World
    • 与其他 location 配合
    • 获取 uri 参数
    • 获取请求 body
    • 输出响应体
    • 日志输出
    • 简单 API Server 框架
    • 使用 Nginx 内置绑定变量
    • 子查询
    • 不同阶段共享变量
    • 防止 SQL 注入
    • 如何发起新 HTTP 请求
    • 如何完成 bit 操作
      • 一,复习二进制补码
      • 二,复习位运算
      • 三,LuaJIT 和 Lua BitOp Api
      • 四,位运算算法实例
      • 五,Lua BitOp 的安装
  • LuaRestyRedisLibrary
    • 访问有授权验证的 Redis
    • select+set_keepalive 组合操作引起的数据读写错误
    • redis 接口的二次封装(简化建连、拆连等细节)
    • redis 接口的二次封装(发布订阅)
    • pipeline 压缩请求数量
    • script 压缩复杂请求
    • 动态生成的 lua-resty-redis 模块方法
  • LuaCjsonLibrary
    • json 解析的异常捕获
    • 稀疏数组
    • 空 table 编码为 array 还是 object
  • PostgresNginxModule
    • 调用方式简介
    • 不支持事务
    • 超时
    • 健康监测
    • SQL 注入
  • LuaNginxModule
    • 执行阶段概念
    • 正确的记录日志
    • 热装载代码
    • 阻塞操作
    • 缓存
    • sleep
    • 定时任务
    • 禁止某些终端访问
    • 请求返回后继续执行
    • 调试
    • 请求中断后的处理
    • 我的 lua 代码需要调优么
    • 变量的共享范围
    • 动态限速
    • shared.dict 非队列性质
    • 正确使用长链接
    • 如何引用第三方 resty 库
    • 典型应用场景
    • 怎样理解 cosocket
    • 如何安全启动唯一实例的 timer
    • 如何正确的解析域名
  • LuaRestyDNSLibrary
    • 使用动态 DNS 来完成 HTTP 请求
  • LuaRestyLock
    • 缓存失效风暴
  • OpenResty 与 SSL
    • HTTPS 时代
    • 动态加载证书和 OCSP stapling
    • TLS session resumption
  • 测试
    • 代码静态分析
    • 单元测试
    • 代码覆盖率
    • API 测试
    • 性能测试
    • 持续集成
    • 灰度发布
      • 分流引擎设计
      • 控制台开发
      • 向运维平台发展
  • Web 服务
    • API 的设计
    • 数据合法性检测
    • 协议无痛升级
    • 代码规范
    • 连接池
    • C10K 编程
    • TIME_WAIT 问题
    • 与 Docker 使用的网络瓶颈
  • 火焰图
    • 什么是火焰图
    • 什么时候使用
    • 如何安装火焰图生成工具
    • 如何定位问题
    • 拓展阅读
    • FAQ
Powered by GitBook
On this page
  • LuaJIT 工作原理
  • 工具和内部对象结构
  • 可以被 JIT 编译的元操作
Edit on GitHub
  1. Lua 高阶

什么是 JIT

PreviousFFINextNginx

Last updated 2 years ago

自从 OpenResty 1.5.8.1 版本之后,默认捆绑的 Lua 解释器就被替换成了 LuaJIT,而不再是标准 Lua。单从名字上,我们就可以直接看到这个新的解释器多了一个 JIT,接下来我们就一起来聊聊 JIT。

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

LuaJIT 工作原理

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、 查看工具和内容解析

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

    102 trace objects: max=928, avg=337, min=160, sum=34468 (in bytes)

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

2、 不足之处

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

  • pairs() 函数

  • unpack() 函数

  • string.match() 函数

  • 基于 lua_CFunction 实现的 Lua C 模块

  • FNEW 字节码,等等。

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

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

3、如何避坑

所谓 让更多的 Lua 代码被 JIT 编译,其实就是帮助更多的 Lua 代码路径能为 JIT 编译器所接受。这一般通过两种途径来实现:

  • 1、 调整对应的 Lua 代码,避免使用 NYI 原语。

  • 2、 增强 JIT 编译器,让越来越多的 NYI 原语能够被编译。

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

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

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

4、实例分析

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

init_by_lua_block {
    local verbose = false
    if verbose then
        local dump = require("jit.dump")
        dump.on(nil, "/tmp/jit.log")
    else
        local v = require("jit.v")
        v.on("/tmp/jit.log")
    end

    require("resty.core")
}

那一行 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 之后,应当使用 ab 和 weighttp 这样的工具对相应的服务接口进行预热,以触发 LuaJIT 的 JIT 编译器开始工作(还记得刚才我们说的 “热函数” 和 “热循环” 吗?)。 预热过程一般不用太久,跑个二三百个请求足矣。当然,压更多的请求也没关系。完事后,我们就可以检查 /tmp/jit.log 文件里面的输出了。

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

  • 1、单行的

    [TRACE   6 shdict.lua:126 return]

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

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

    [TRACE  16 (15/1) waf-core.lua:419 -> 15]

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

  • 3、被中断的 而下面这个例子则是被中断的 trace:

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

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

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

    [TRACE --- exit.lua:27 -- NYI: FastFunc coroutine.yield at waf-core.lua:439]
    [TRACE --- waf.lua:321 -- NYI: bytecode 51 at raven.lua:107]

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

5、 探查字节码的工具

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

# /usr/local/openresty/luajit/bin/luajit-2.1.0-alpha ljbc.lua 51
opcode 51:
FNEW

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

ljbc.lua 脚本的位置是:

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

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

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

6、对比实验

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

# cat test.lua
local s = [[aaaaaabbbbbbbcccccccccccddddddddddddeeeeeeeeeeeee
fffffffffffffffffggggggggggggggaaaaaaaaaaabbbbbbbbbbbbbb
ccccccccccclllll]]

for i=1,10000 do
    for j=1,10000 do
        string.find(s, "ll", 1, true)
    end
end

# time luajit test.lua
5.19s user
0.03s system
96% cpu
5.392 total

#  time lua test.lua
9.20s user
0.02s system
99% cpu
9.270 total

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

文件 test.lua:


local loop_count = tonumber(arg[1])
local fun_pair = "ipairs" == arg[2] and ipairs or pairs

local t = {}
for i=1,100 do
    t[i] = i
end

for i=1,loop_count do
    for j=1,1000 do
        for k,v in fun_pair(t) do
            --
        end
    end
end
执行参数
执行结果

(下面三行是 ipairs 测试结果)

time lua test.lua 1000 ipairs

3.96s user 0.02s system 98% cpu 4.039 total

time luajit test.lua 1000 ipairs

0.10s user 0.00s system 95% cpu 0.113 total

time luajit test.lua 10000 ipairs

0.98s user 0.00s system 99% cpu 0.991 total

(下面两行是 pairs 测试结果)

time lua test.lua 1000 pairs

3.97s user 0.01s system 99% cpu 3.992 total

time luajit test.lua 1000 pairs

1.54s user 0.01s system 99% cpu 1.559 total

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

  • 在标准 Lua 解释器中,使用 ipairs 或 pairs 没有区别;

  • 对于 pairs 方式,LuaJIT 的性能大约是标准 Lua 的 4 倍;

  • 对于 ipairs 方式,LuaJIT 的性能大约是标准 Lua 的 40 倍。

可以被 JIT 编译的元操作

基础库的支持情况

函数
编译?
备注

assert

yes

collectgarbage

no

dofile

never

error

never

getfenv

2.1 partial

只有 getfenv(0) 能编译

getmetatable

yes

ipairs

yes

load

never

loadfile

never

loadstring

never

next

no

pairs

no

pcall

yes

print

no

rawequal

yes

rawget

yes

rawlen (5.2)

yes

rawset

yes

select

partial

第一个参数是静态变量的时候可以编译

setfenv

no

setmetatable

yes

tonumber

partial

不能编译非10进制,非预期的异常输入

tostring

partial

只能编译:字符串、数字、布尔、nil 以及支持 __tostring元方法的类型

type

yes

unpack

no

xpcall

yes

字符串库

函数
编译?
备注

string.byte

yes

string.char

2.1

string.dump

never

string.find

2.1 partial

只有字符串样式查找(没有样式)

string.format

2.1 partial

不支持 %p 或 非字符串参数的 %s

string.gmatch

no

string.gsub

no

string.len

yes

string.lower

2.1

string.match

no

string.rep

2.1

string.reverse

2.1

string.sub

yes

string.upper

2.1

表

函数
编译?
备注

table.concat

2.1

table.foreach

no

2.1: 内部编译,但还没有外放

table.foreachi

2.1

table.getn

yes

table.insert

partial

只有 push 操作

table.maxn

no

table.pack (5.2)

no

table.remove

2.1

部分,只有 pop 操作

table.sort

no

table.unpack (5.2)

no

math 库

函数
编译?
备注

math.abs

yes

math.acos

yes

math.asin

yes

math.atan

yes

math.atan2

yes

math.ceil

yes

math.cos

yes

math.cosh

yes

math.deg

yes

math.exp

yes

math.floor

yes

math.fmod

no

math.frexp

no

math.ldexp

yes

math.log

yes

math.log10

yes

math.max

yes

math.min

yes

math.modf

yes

math.pow

yes

math.rad

yes

math.random

yes

math.randomseed

no

math.sin

yes

math.sinh

yes

math.sqrt

yes

math.tan

yes

math.tanh

yes

1、 你可以通过 ngx-lj-gc-objs 工具看到指定的 Nginx worker 进程里所有 trace 对象的一些基本的统计信息,见

下面给大家列一下截止到目前已经可以被 JIT 编译的元操作。 其他还有 IO、Bit、FFI、Coroutine、OS、Package、Debug、JIT 等分类,使用频率相对较低,这里就不罗列了,可以参考官网:。

http://wiki.luajit.org/Bytecode-2.0
https://github.com/openresty/stapxx#ngx-lj-gc-objs
http://wiki.luajit.org/NYI