极客时间已完结课程限时免费阅读

10 | JIT编译器的死穴:为什么要避免使用 NYI ?

10 | JIT编译器的死穴:为什么要避免使用 NYI ?-极客时间

10 | JIT编译器的死穴:为什么要避免使用 NYI ?

讲述:温铭

时长12:39大小11.56M

你好,我是温铭。
上一节,我们一起了解了 LuaJIT 中的 FFI。如果你的项目中只用到了 OpenResty 提供的 API,没有自己调用 C 函数的需求,那么 FFI 对你而言并没有那么重要,你只需要确保开启了 lua-resty-core 即可。
但我们今天要讲的 LuaJIT 中 NYI,却是每一个使用 OpenResty 的工程师都逃避不了的关键问题,它对于性能的影响举足轻重。
你可以很快使用 OpenResty 写出逻辑正确的代码,但不明白 NYI,你就不能写出高效的代码,无法发挥 OpenResty 真正的威力。这两者的性能差距,至少是一个数量级的。

什么是 NYI?

那究竟什么是 NYI 呢?先回顾下我们之前提到过的一个知识点:
LuaJIT 的运行时环境,除了一个汇编实现的 Lua 解释器外,还有一个可以直接生成机器代码的 JIT 编译器。
LuaJIT 中 JIT 编译器的实现还不完善,有一些原语它还无法编译,因为这些原语实现起来比较困难,再加上 LuaJIT 的作者目前处于半退休状态。这些原语包括常见的 pairs() 函数、unpack() 函数、基于 Lua CFunction 实现的 Lua C 模块等。这样一来,当 JIT 编译器在当前代码路径上遇到它不支持的操作时,便会退回到解释器模式。
而 JIT 编译器不支持的这些原语,其实就是我们今天要讲的 NYI,全称为 Not Yet Implemented。LuaJIT 的官网上有这些 NYI 的完整列表,建议你仔细浏览一遍。当然,目的不是让你背下这个列表的内容,而是让你要在写代码的时候有意识地提醒自己。
下面,我截取了 NYI 列表中 string 库的几个函数:
其中,string.byte 对应的能否被编译的状态是 yes,表明可以被 JIT,你可以放心大胆地在代码中使用。
string.char 对应的编译状态是 2.1,表明从 LuaJIT 2.1 开始支持。我们知道,OpenResty 中的 LuaJIT 是基于 LuaJIT 2.1 的,所以你也可以放心使用。
string.dump 对应的编译状态是 never,即不会被 JIT,会退回到解释器模式。目前来看,未来也没有计划支持这个原语。
string.find 对应的编译状态是 2.1 partial,意思是从 LuaJIT 2.1 开始部分支持,后面的备注中写的是 只支持搜索固定的字符串,不支持模式匹配。所以对于固定字符串的查找,你使用 string.find 是可以被 JIT 的。
我们自然应该避免使用 NYI,让更多的代码可以被 JIT 编译,这样性能才能得到保证。但在现实环境中,我们有时候不可避免要用到一些 NYI 函数的功能,这时又该怎么办呢?

NYI 的替代方案

其实,不用担心,大部分 NYI 函数我们都可以敬而远之,通过其他方式来实现它们的功能。接下来,我挑选了几个典型的 NYI 来讲解,带你了解不同类型的 NYI 替代方案。这样,其他的 NYI 你也可以自己触类旁通。

1.string.gsub() 函数

第一个我们来看 string.gsub() 函数。它是 Lua 内置的字符串操作函数,作用是做全局的字符串替换,比如下面这个例子:
$ resty -e 'local new = string.gsub("banana", "a", "A"); print(new)'
bAnAnA
这个函数是一个 NYI 原语,无法被 JIT 编译。
我们可以尝试在 OpenResty 自己的 API 中寻找替代函数,但对于大多数人来说,记住所有的 API 和用法是不现实的。所以在平时开发中,我都会打开 lua-nginx-module 的 GitHub 文档页面
比如,针对刚刚的这个例子,我们可以用 gsub 作为关键字,在文档页面中搜索,这时ngx.re.gsub 就会映入眼帘。
细心的同学可能会问,这里为什么不用之前推荐的 restydoc 工具,来搜索 OpenResty API 呢?你可以尝试下用它来搜索 gsub
$ restydoc -s gsub
看到了吧,这里并没有返回我们期望的 ngx.re.gsub,而是显示了 Lua 自带的函数。事实上,现阶段而言, restydoc 返回的是唯一的精准匹配的结果,所以它更适合在你明确知道 API 名字的前提下使用。至于模糊的搜索,还是要自己手动在文档中进行。
回到刚刚的搜索结果,我们看到,ngx.re.gsub 的函数定义如下:
newstr, n, err = ngx.re.gsub(subject, regex, replace, options?)
这里,函数参数和返回值的命名都带有具体的含义。其实,在 OpenResty 中,我并不推荐你写很多注释,大多数时候,一个好的命名胜过好几行注释。
对于不熟悉 OpenResty 正则体系的工程师而言,看到最后的变参 options ,你可能会比较困惑。不过,这个变参的解释,并不在此函数中,而是在 ngx.re.match 函数的文档中。
通过查看参数 options 的文档,你会发现,只要我们把它设置为 jo,就开启了 PCRE 的 JIT。这样,使用 ngx.re.gsub 的代码,既可以被 LuaJIT 进行 JIT 编译,也可以被 PCRE JIT 进行 JIT 编译。
具体的文档内容,我就不再赘述了。不过这里我想强调一点——在翻看文档时,我们一定要有打破砂锅问到底的精神。OpenResty 的文档其实非常完善,仔细阅读文档,就可以解决你大部分的问题。

2.string.find() 函数

string.gsub 不同的是,string.find 在 plain 模式(即固定字符串的查找)下,是可以被 JIT 的;而带有正则这种的字符串查找,string.find 并不能被 JIT ,这时就要换用 OpenResty 自己的 API,也就是 ngx.re.find 来完成。
所以,当你在 OpenResty 中做字符串查找时,首先一定要明确区分,你要查找的是固定的字符串,还是正则表达式。如果是前者,就要用 string.find,并且记得把最后的 plain 设置为 true:
string.find("foo bar", "foo", 1, true)
如果是后者,你应该用 OpenResty 自己的 API,并开启 PCRE 的 JIT 选项:
ngx.re.find("foo bar", "^foo", "jo")
其实,这里更适合做一层封装,并把优化选项默认打开,不要让最终的使用者知道这么多细节。这样,对外就是统一的字符串查找函数了。你可以感受到,有时候选择太多、太灵活并不是一件好事。

3.unpack() 函数

第三个我们来看 unpack() 函数。unpack() 也是要避免使用的函数,特别是不要在循环体中使用。你可以改用数组的下标去访问,比如下面代码的这个例子:
$ resty -e '
local a = {100, 200, 300, 400}
for i = 1, 2 do
print(unpack(a))
end'
$ resty -e 'local a = {100, 200, 300, 400}
for i = 1, 2 do
print(a[1], a[2], a[3], a[4])
end'
让我们再深究一下 unpack,这次我们可以用restydoc 来搜索一下:
$ restydoc -s unpack
从 unpack 的文档中,你可以看出,unpack (list [, i [, j]])return list[i], list[i+1], , list[j] 是等价的,你可以把 unpack 看成一个语法糖。这样,你完全可以用数组下标的方式来访问,以免打断 LuaJIT 的 JIT 编译。

4.pairs() 函数

最后我们来看遍历哈希表的 pairs() 函数,它也不能被 JIT 编译。
不过非常遗憾,这个并没有等价的替代方案,你只能尽量避免使用,或者改用数字下标访问的数组,特别是在热代码路径上不要遍历哈希表。这里我解释一下代码热路径,它的意思是,这段代码会被返回执行很多次,比如在一个很大的循环里面。
说完这四个例子,我们来总结一下,要想规避 NYI 原语的使用,你需要注意下面这两点:
请优先使用 OpenResty 提供的 API,而不是 Lua 的标准库函数。这里要牢记, Lua 是嵌入式语言,我们实际上是在 OpenResty 中编程,而不是 Lua。
如果万不得已要使用 NYI 原语,请一定确保它没有在代码热路径上。

如何检测 NYI?

讲了这么多 NYI 的规避方案,都是在教你该怎么做。不过,如果到这里戛然而止,那就不太符合 OpenResty 奉行的一个哲学:
能让机器自动完成的,就不要人工参与。
人不是机器,总会有疏漏,能够自动化地检测代码中使用到的 NYI,才是工程师价值的一个重要体现。
这里我推荐,LuaJIT 自带的 jit.dumpjit.v 模块。它们都可以打印出 JIT 编译器工作的过程。前者会输出非常详细的信息,可以用来调试 LuaJIT 本身,你可以参考它的源码来做更深入的了解;后者的输出比较简单,每行对应一个 trace,通常用来检测是否可以被 JIT。
具体应该怎么操作呢?
我们可以先在 init_by_lua 中,添加以下两行代码:
local v = require "jit.v"
v.on("/tmp/jit.log")
然后,运行你自己的压力测试工具,或者跑几百个单元测试集,让 LuaJIT 足够热,触发 JIT 编译。这些都完成后,再来检查 /tmp/jit.log 的结果。
当然,这个方法相对比较繁琐,如果你想要简单验证的话, 使用 resty 就足够了,这个 OpenResty 的 CLI 带有相关选项:
$resty -j v -e 'for i=1, 1000 do
local newstr, n, err = ngx.re.gsub("hello, world", "([a-z])[a-z]+", "[$0,$1]", "i")
end'
[TRACE 1 (command line -e):1 stitch C:107bc91fd]
[TRACE 2 (1/stitch) (command line -e):2 -> 1]
其中,resty-j 就是和 LuaJIT 相关的选项;后面的值为 dumpv,就对应着开启 jit.dumpjit.v 模式。
在 jit.v 模块的输出中,每一行都是一个成功编译的 trace 对象。刚刚是一个能够被 JIT 的例子,而如果遇到 NYI 原语,输出里面就会指明 NYI,比如下面这个 pairs 的例子:
$resty -j v -e 'local t = {}
for i=1,100 do
t[i] = i
end
for i=1, 1000 do
for j=1,1000 do
for k,v in pairs(t) do
--
end
end
end'
它就不能被 JIT,所以结果里,指明了第 8 行中有 NYI 原语。
[TRACE 1 (command line -e):2 loop]
[TRACE --- (command line -e):7 -- NYI: bytecode 72 at (command line -e):8]

写在最后

这是我们第一次用比较多的篇幅来谈及 OpenResty 的性能问题。看完这些关于 NYI 的优化,不知道你有什么感想呢?可以留言说说你的看法。
最后,给你留一道思考题。在讲 string.find() 函数的替代方案时,我有提到过,那里其实更适合做一层封装,并默认打开优化选项。那么,这个任务就交给你来小试牛刀了。
欢迎在留言区写下你的答案,也欢迎你把这篇文章分享给你的同事、朋友,一起交流,一起进步。
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 4

提建议

上一篇
09 | 为什么 lua-resty-core 性能更高一些?
下一篇
11 | 剖析Lua唯一的数据结构table和metatable特性
unpreview
 写留言

精选留言(23)

  • 2019-06-17
    对于一个OpenResty入门者一开始讲这些性能和低层的知识有些枯燥了,毕竟还没写过几个OpenResty的实例,对性能的差异没什么感觉,基础知识还没掌握全更难深入底层。有如学习java的初学者一开始就看《Java编程思想》。可否尝试在后面的实际例子中引出这些知识点。

    作者回复: 后面会专门介绍 OpenResty 的 Lua API。LuaJIT 的这些内容有个印象即可。

    17
  • HelloBug
    2019-06-17
    封装字符串查找函数: local function new_string_find(src, dst, regex, pos) if regex then local ctx = {pos = pos or 1} return ngx.re.find(src, dst, "jo", ctx) else local pos = pos or 1 return string.find(src, dst, pos, true) end end
    展开
    9
  • 燕羽阳
    2019-06-17
    1. lua的语法比较简单,如果有编程基础的话,一天就可以入门了。推荐大家两本书《lua程序设计》lua入门必备,《lua设计与实现》深入解释器虚拟机的原理。 2. 之前写过一点openresty和kong,但是从没注意过性能问题。今天的NYI,老师讲的非常棒,完整的实战方案,超赞👍 3.我对jit完全不熟悉,请问老师,jit是按照函数为单位来编译么?函数中有一个NYI,整个函数就是解释运行么?
    展开

    作者回复: 是按照原语来编译的,也就是 `ipairs` 、`string.find` 这种的颗粒度,并不是你自己写的 function

    共 2 条评论
    6
  • geekYang
    2020-10-15
    老师,NYI 的替代方案为什么不去看 lua-resty-core,而要在lua-nginx-module中寻找? ngx.re.gsub 为什么即可以pcre编译,也可以luajit 编译?
    3
  • helloworld
    2019-06-17
    老师,下面这个设置lua库的搜索路径的代码,最后为什么要拼接package.path本身啊? package.path = "../myLuaTest/myLuaCode/?.lua;"..package.path

    作者回复: package.path 是默认的查找路径,你可以单独把它 print 出来看下里面的值。不拼接的话,就把默认查找路径全都覆盖掉了。

    3
  • Geek_89bbab
    2019-06-17
    希望老师分享一下写 openresty代码时用什么编辑器比较好,代码补全,定义跳转,引用跳转等如何配置。老师你们写openresty的时候是怎么来做的?直接vim操作,还是有更智能些的IDE?

    作者回复: 现在还没啥好用的编辑器,我用的是 vs code

    3
  • Geek_2b5c15
    2022-01-14
    Accroding from the new NYI list, the paris have been implement.
    3
  • NEVER SETTLE
    2019-06-19
    经常会发现openresty运行一段时间,性能会差很多。然后reload一下性能会好很多。这个问题一直困扰很久了,不知道如何进行排查。 以前两天的情况,高峰期CPU使用率超过了60%,然后reload一下,CPU使用率就降为30%。

    作者回复: 需要用火焰图分析下 on-cpu 才行

    2
  • Forturn
    2019-06-17
    楼上说的有道理,目前还没掌握基本的语法,还不会写一些基本的功能,就去学底层的东西,有点摸不着头脑,也不懂这些。不知道后面会不会具体事例

    作者回复: 看不懂没有关系,重要的是记得有LuaJIT 和 NYI 这个东西,后面遇到问题方便查找。

    2
  • MiaoVictor
    2021-05-07
    老师,请问在查看NYI列表是,某些原语后是“2.1 stitch”,这个stitch是什么意思呢?
    1
  • KoALa
    2019-07-25
    教程中反复提到了“LuaJIT 的作者目前处于半退休状态”,感觉这个情况很不乐观啊...

    作者回复: Kong 和 OpenResty 的团队中,都有人在逐步接手。开源项目只要有人在使用,就不会死掉,不用担心。

    1
  • 莫然
    2019-07-11
    pairs的例子稍作修改,如下: local t = {} for i=1,100 do t[i] = i end for j=1,10 do for k,v in pairs(t) do -- end end jit.log里输出的是:[TRACE 1 t.lua:123 loop] 如果改成如下代码: for j=1,100 do for k,v in pairs(t) do -- end end jit.log里输出的是: [TRACE 1 t.lua:123 loop] [TRACE --- t.lua:128 -- NYI: bytecode 72 at t.lua:129] [TRACE --- t.lua:128 -- NYI: bytecode 72 at t.lua:129] 为什么循环10的时候没有NYI的提示?
    展开

    作者回复: LuaJIT 的优化是随机触发的,要足够热才可能尝试去优化

    1
  • John
    2019-06-18
    老师,我在用op实现一个自定义逻辑的风控系统,想请教一下动态配置生效的问题,如何在不reload的情况下使配置生效,我想了两个办法:1,使用全局变量存储配置文件,提供一个api更新全局变量;2,使用ngx.shared.DICT,将配置文件存储在共享内存中。请问是否合理,或者是否有其它思路

    作者回复: 你可以使用一个 timer,定时的查询是否有新的配置,并把新配置写到类似 shared dict 的缓存中。

    共 2 条评论
    1
  • 涉蓝
    2019-06-17
    对于初学者是不是 要先学 lua -> luajit -> openresty api -> 其他第三方包 按这种先把 文档啥的都扒一遍才行呢? 我对于使用有点疑惑,openresty可以做 网站普通后端语言可以做的事 譬如连数据库做前端页面 但这显然并不是它主要适合的部分吧 毕竟其他后端语言一大把,文档生态库好的多的是 所以是API 网关的 开发 或者 nginx 无法配置热修改的 补充吗?
    展开

    作者回复: 这个问题其实要回到 OpenResty 诞生的那个时候来看, 2007 年,支持同步非阻塞的语言凤毛麟角。 即使是现在,后端语言可以达到 OpenResty 这种性能级别的也不多。 API 网关和软 WAF 算是开发者的自然选择,OpenResty 其实能做的不止这些。

    1
  • John
    2019-06-17
    请教一下老师,当op中的lua规则和nginx配置文件产生冲突,比如nginx配置了rewrite规则,又同时引用了rewrite_by_lua_file,那么这两条规则的优先级是什么?

    作者回复: 这个具体要看 nginx 配置的 rewrite 规则是怎么写的了,是 break 还是 last。这个在官方文档中有注明,并且配了一个示例代码: location /foo { rewrite ^ /bar; rewrite_by_lua 'ngx.exit(503)'; } location /bar { ... } 上面这个配置中,ngx.exit(503) 是不会被执行的。但是,如果改成: rewrite ^ /bar break; ngx.exit(503) 就是可以执行的。

    共 2 条评论
    1
  • skrbug
    2023-01-29 来自北京
    LuaJit 的NYI,官网链接失效了老师
  • 张靳
    2022-02-02
    老师,我想问下,如果是一个完整的API网关,里面写了很多的lua模块,如何比较简单快速的排查NYI?
  • 沈康
    2020-09-25
    老师讲的非常好,做技术就要深挖底层原理,而且目前讲的也没有到底层 小白就知道急功近利,我只能说项目有问题谁来背锅?
  • Geek_e59303
    2019-11-12
    什么是代码热路径啊
  • sheng
    2019-08-09
    这里的一个数量级是指十倍的意思么?

    作者回复: 是的