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

08 | LuaJIT分支和标准Lua有什么不同?

08 | LuaJIT分支和标准Lua有什么不同?-极客时间

08 | LuaJIT分支和标准Lua有什么不同?

讲述:温铭

时长10:38大小8.53M

你好,我是温铭。
这节课,我们来学习下 OpenResty 的另一块基石:LuaJIT。今天主要的篇幅,我会留给 Lua 和 LuaJIT 中重要和鲜为人知的一些知识点。而更多 Lua 语言的基础知识,你可以通过搜索引擎或者 Lua 的书籍自己来学习,这里我推荐 Lua 作者编写的《Lua 程序设计》这本书。
当然,在 OpenResty 中,写出正确的 LuaJIT 代码的门槛并不高,但要写出高效的 LuaJIT 代码绝非易事,这里的关键内容,我会在后面 OpenResty 性能优化部分详细介绍。
我们先来看下 LuaJIT 在 OpenResty 整体架构中的位置:
前面我们提到过,OpenResty 的 worker 进程都是 fork master 进程而得到的, 其实, master 进程中的 LuaJIT 虚拟机也会一起 fork 过来。在同一个 worker 内的所有协程,都会共享这个 LuaJIT 虚拟机,Lua 代码的执行也是在这个虚拟机中完成的。
这可以算是 OpenResty 的基本原理,后面课程我们再详细聊聊。今天我们先来理顺 Lua 和 LuaJIT 的关系。

标准 Lua 和 LuaJIT 的关系

先把重要的事情放在前面说:
标准 Lua 和 LuaJIT 是两回事儿,LuaJIT 只是兼容了 Lua 5.1 的语法。
标准 Lua 现在的最新版本是 5.3,LuaJIT 的最新版本则是 2.1.0-beta3。在 OpenResty 几年前的老版本中,编译的时候,你可以选择使用标准 Lua VM ,或者 LuaJIT VM 来作为执行环境,不过,现在已经去掉了对标准 Lua 的支持,只支持 LuaJIT。
LuaJIT 的语法兼容 Lua 5.1,并对 Lua 5.2 和 5.3 做了选择性支持。所以我们应该先学习 Lua 5.1 的语法,并在此基础上学习 LuaJIT 的特性。上节课我已经带你入门了 Lua 的基础语法,今天只提及 Lua 的一些特别之处。
值得注意的是,OpenResty 并没有直接使用 LuaJIT 官方提供的 2.1.0-beta3 版本,而是在此基础上,扩展了自己的 fork: [openresty-luajit2]:
OpenResty 维护了自己的 LuaJIT 分支,并扩展了很多独有的 API。
这些独有的 API,都是在实际开发 OpenResty 的过程中,出于性能方面的考虑而增加的。所以,我们后面提到的 LuaJIT,特指 OpenResty 自己维护的 LuaJIT 分支。

为什么选择 LuaJIT?

说了这么多 LuaJIT 和 Lua 的关系,你可能会纳闷儿,为什么不直接使用 Lua,而是要用自己维护的 LuaJIT 呢?其实,最主要的原因,还是 LuaJIT 的性能优势。
其实标准 Lua 出于性能考虑,也内置了虚拟机,所以 Lua 代码并不是直接被解释执行的,而是先由 Lua 编译器编译为字节码(Byte Code),然后再由 Lua 虚拟机执行。
而 LuaJIT 的运行时环境,除了一个汇编实现的 Lua 解释器外,还有一个可以直接生成机器代码的 JIT 编译器。开始的时候,LuaJIT 和标准 Lua 一样,Lua 代码被编译为字节码,字节码被 LuaJIT 的解释器解释执行。
但不同的是,LuaJIT 的解释器会在执行字节码的同时,记录一些运行时的统计信息,比如每个 Lua 函数调用入口的实际运行次数,还有每个 Lua 循环的实际执行次数。当这些次数超过某个随机的阈值时,便认为对应的 Lua 函数入口或者对应的 Lua 循环足够热,这时便会触发 JIT 编译器开始工作。
JIT 编译器会从热函数的入口或者热循环的某个位置开始,尝试编译对应的 Lua 代码路径。编译的过程,是把 LuaJIT 字节码先转换成 LuaJIT 自己定义的中间码(IR),然后再生成针对目标体系结构的机器码。
所以,所谓 LuaJIT 的性能优化,本质上就是让尽可能多的 Lua 代码可以被 JIT 编译器生成机器码,而不是回退到 Lua 解释器的解释执行模式。明白了这个道理,你才能理解后面学到的 OpenResty 性能优化的本质。

Lua 特别之处

正如我们上节课介绍的一样,Lua 语言相对简单。对于有其他开发语言背景的工程师来说,注意 到 Lua 中一些独特的地方后,你就能很容易的看懂代码逻辑。接下来,我们一起来看 Lua 语言比较特别的几个地方。

1. Lua 的下标从 1 开始

Lua 是我知道的唯一一个下标从 1 开始的编程语言。这一点,虽然对于非程序员背景的人来说更好理解,但却容易导致程序的 bug。
下面是一个例子:
$ resty -e 't={100}; ngx.say(t[0])'
你自然期望打印出 100,或者报错说下标 0 不存在。但结果出乎意料,什么都没有打印出来,也没有报错。既然如此,让我们加上 type 命令,来看下输出到底是什么:
$ resty -e 't={100};ngx.say(type(t[0]))'
nil
原来是空值。事实上,在 OpenResty 中,对于空值的判断和处理也是一个容易让人迷惑的点,后面我们讲到 OpenResty 的时候再细聊。

2. 使用 .. 来拼接字符串

这一点,上节课我也提到过。和大部分语言使用 + 不同,Lua 中使用两个点号来拼接字符串:
$ resty -e "ngx.say('hello' .. ', world')"
hello, world
在实际的项目开发中,我们一般都会使用多种开发语言,而 Lua 这种不走寻常路的设计,总是会让开发者的思维,在字符串拼接的时候卡顿一下,也是让人哭笑不得。

3. 只有 table 这一种数据结构

不同于 Python 这种内置数据结构丰富的语言,Lua 中只有一种数据结构,那就是 table,它里面可以包括数组和哈希表:
local color = {first = "red", "blue", third = "green", "yellow"}
print(color["first"]) --> output: red
print(color[1]) --> output: blue
print(color["third"]) --> output: green
print(color[2]) --> output: yellow
print(color[3]) --> output: nil
如果不显式地用_键值对_的方式赋值,table 就会默认用数字作为下标,从 1 开始。所以 color[1] 就是 blue。
另外,想在 table 中获取到正确长度,也是一件不容易的事情,我们来看下面这些例子:
local t1 = { 1, 2, 3 }
print("Test1 " .. table.getn(t1))
local t2 = { 1, a = 2, 3 }
print("Test2 " .. table.getn(t2))
local t3 = { 1, nil }
print("Test3 " .. table.getn(t3))
local t4 = { 1, nil, 2 }
print("Test4 " .. table.getn(t4))
使用 resty 运行的结果如下:
Test1 3
Test2 2
Test3 1
Test4 1
你可以看到,除了第一个返回长度为 3 的测试案例外,后面的测试都是我们预期之外的结果。事实上,想要在 Lua 中获取 table 长度,必须注意到,只有在 table 是 _序列_ 的时候,才能返回正确的值。
那什么是序列呢?首先序列是数组(array)的子集,也就是说,table 中的元素都可以用正整数下标访问到,不存在键值对的情况。对应到上面的代码中,除了 t2 外,其他的 table 都是 array。
其次,序列中不包含空洞(hole),即 nil。综合这两点来看,上面的 table 中, t1 是一个序列,而 t3 和 t4 是 array,却不是序列(sequence)。
到这里,你可能还有一个疑问,为什么 t4 的长度会是 1 呢?其实这是因为,在遇到 nil 时,获取长度的逻辑就不继续往下运行,而是直接返回了。
不知道你完全看懂了吗?这部分确实相当复杂。那么有没有什么办法可以获取到我们想要的 table 长度呢?自然是有的,OpenResty 在这方面做了扩展,在后面专门的 table 章节我会讲到,这里先留一个悬念。

4. 默认是全局变量

我想先强调一点,除非你相当确定,否则在 Lua 中声明变量时,前面都要加上 local
local s = 'hello'
这是因为在 Lua 中,变量默认是全局的,会被放到名为 _G 的 table 中。不加 local 的变量会在全局表中查找,这是昂贵的操作。如果再加上一些变量名的拼写错误,就会造成难以定位的 bug。
所以,在 OpenResty 编程中,我强烈建议你总是使用 local 来声明变量,即使在 require module 的时候也是一样:
-- Recommended
local xxx = require('xxx')
-- Avoid
require('xxx')

LuaJIT

明白了 Lua 这四点特别之处,我们继续来说 LuaJIT。除了兼容 Lua 5.1 的语法并支持 JIT 外,LuaJIT 还紧密结合了 FFI(Foreign Function Interface),可以让你直接在 Lua 代码中调用外部的 C 函数和使用 C 的数据结构。
下面是一个最简单的例子:
local ffi = require("ffi")
ffi.cdef[[
int printf(const char *fmt, ...);
]]
ffi.C.printf("Hello %s!", "world")
短短这几行代码,就可以直接在 Lua 中调用 C 的 printf 函数,打印出 Hello world!。你可以使用 resty 命令来运行它,看下是否成功。
类似的,我们可以用 FFI 来调用 NGINX、OpenSSL 的 C 函数,来完成更多的功能。实际上,FFI 方式比传统的 Lua/C API 方式的性能更优,这也是 lua-resty-core 项目存在的意义。下一节我们就来专门讲讲 FFI 和 lua-resty-core
此外,出于性能方面的考虑,LuaJIT 还扩展了 table 的相关函数:table.newtable.clear这是两个在性能优化方面非常重要的函数,在 OpenResty 的 lua-resty 库中会被频繁使用。不过,由于相关文档藏得非常深,而且没有示例代码,所以熟悉它们的开发者并不多。我们留到性能优化章节专门来讲它们。

写在最后

让我们来回顾下今天的内容。
OpenResty 出于性能的考虑,选择了 LuaJIT 而不是标准 Lua,并且维护了自己的 LuaJIT 分支。而 LuaJIT 基于 Lua 5.1 的语法,并选择性地兼容了部分 Lua5.2 和 Lua5.3 的语法,形成了自己的体系。至于你需要掌握的 Lua 语法,在下标、字符串拼接、数据结构和变量上,都有自己鲜明的特点,在写代码的时候你应该特别留意。
你在学习 Lua 和 LuaJIT 的时候,是否遇到一些陷阱和坑呢?欢迎留言一起来聊一聊,我在后面也专门写了一篇文章,来分享我遇到过的那些坑。也欢迎你把这篇文章分享给你的同事、朋友,一起学习,一起进步。
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 10

提建议

上一篇
07 | 带你快速上手 Lua
下一篇
09 | 为什么 lua-resty-core 性能更高一些?
unpreview
 写留言

精选留言(20)

  • 轨迹
    2019-06-12
    老师可以讲一下table的内部结构,把原理搞清楚,就比较容易了。 譬如这个文章:https://blog.csdn.net/wwlcsdn000/article/details/81291756 简单的就是两点: 1、两种存储,哈希和数组。 2、数组以下标覆盖到哈希,如果遇到key冲突,数组覆盖哈希的value。 我个人感觉就更好一点。
    展开

    作者回复: 多谢补充 :)

    18
  • 空知
    2019-06-13
    resty -e "local t = {1, nil, name='cjf', 2} print(table.getn(t))" 3 老师 测试下这个 咋会出来3呢 不是遇见nil就停止了吗
    共 5 条评论
    7
  • J.Smile
    2019-06-12
    例子讲的还是挺清楚的,希望继续讲一下具体openresty实际项目的使用场景案例
    5
  • 逍遥
    2019-07-08
    openresty为什么要维护自己的luajit分支呢,为什么不能用luajit

    作者回复: 有两个原因:一个是 LuaJIT 的作者基本处于退休状态,只修 bug,不怎么加新功能,关于 LuaJIT 的 bug,OpenResty 还是会提交给LuaJIT 官方的;第二个是新增的主要是 OpenResty 优化中遇到的 API,自己维护更容易控制版本和节奏。

    4
  • 一步
    2019-06-12
    上面说的 luajit 先把 字节码转为中间码爱转为 机器码 这个有个以为你,字节码不就是机器码吗? 不都是二进制的东西吗

    作者回复: 这里的字节码是 LuaJIT 虚拟机执行的一种指令格式;机器码是指 CPU 可以读取的指令格式。

    3
  • 岁月如歌
    2019-07-13
    @空知 提出的问题 有相同的疑问? 【 resty -e "local t = {1, nil, name='cjf', 2} print(table.getn(t))" 3 老师 测试下这个 咋会出来3呢 不是遇见nil就停止了吗 】
    展开

    作者回复: 我建议大家绕着走,把 nil 改为 ngx.null 来填充数组。不同的 lua 版本会有不同的行为,我也不太清楚。

    共 5 条评论
    1
  • 旺旺
    2019-06-12
    1."在同一个 worker 内的所有协程",协程就是常说的线程吧? 2.$ resty -e 't={100}; ngx.say(t[0])' 这代码,变量定义前面需要加local才能运行。 3.“对应到上面的代码中,除了 t2 外,其他的 table 都是 array。” 这句话写反了吧,应该是“除了 t2 外,其他的 table 都是 序列。”吧
    1
  • 林潇
    2022-05-20
    学习lua的时候遇到几个不一样的地方: 1. if必须要有end,在python和lua之间切换会很不习惯。 2. 一般一个对象访问属性是用冒号:,而不是点.,也会经常性写错。
  • Leo
    2021-12-01
    我遇到的坑是在删除table的时候,用数组下标循环删除成员时,每次只删除一个还好,当删除两个连续成员的时候就会成问题 后来发现了,检测第二个元素的时候,已经因为上一个元素的删除,导致后一个符合条件的元素往前窜了一个,就想队列一样,导致元素变量索引减1,导致删不掉的bug 后来网上找了下成功经验,把队列模型改成堆栈模型,循环从小变大的规则,改成从大到小循环,即便当前索引的元素删掉,最多影响处理完的数据下标发生变化,不会影响到未处理的元素,挺有意思的。
    展开
  • Geek_xiaoer
    2021-05-10
    请问,Lua虚拟机跟Lua解释器是一样的吗,只是表述不同?
  • wedvefv
    2021-04-20
    接口文件和模块文件都需要 local print=print吗? 为了避免冲全局表找变量吗?
  • 2020-08-16
    这个luajit,不是和java的hotspot一样么,just-in-time,一些是解释执行,然后对于一些热点代码,进行编译执行,这是和java hotspot虚拟机一样的机制,不知道是谁学习的谁
    1
  • 雪粮
    2020-07-24
    对于LuaJIT VM, 可以理解为Master进程和它Fork的每个子进程中都有一个独立的LuaJIT VM吗?
  • HelloTalk
    2019-10-20
    原来默认写lua 的时候,需要做二进制 位移、抑或的操作,为此自己写了一个bitop的库,发现性能更不上,后面上了 luajit,测试快了很多。
  • KoALa
    2019-07-24
    lua if语句的判断方式和ruby貌似一样,nil和false才为假,其他都为真,用多了个人觉得这种判断方式更合理~~

    作者回复: 主要是OpenResty 中空值的情况比较多

  • 杨丁
    2019-07-12
    内容通俗易懂,赞。老师什么时候出个限流的实战吧

    作者回复: 后面有专门的限流限速章节

  • HelloBug
    2019-06-15
    有遇到一些让人困惑的地方是ngx.null、nil、null、“”。在网上搜索的时候,有看到说null是ngx.null的一个定义。redis的返回值的时候,经常会判断返回结果是否为空,判断的时候是和哪个值进行比较呢?关于这些个值有没有其他一些使用上的坑呢?一直以来都没有有一个明确的认识,所以和老师确认一下。

    作者回复: 在 lua-resty-redis 里面,查找一个key 的时候: local res, err = red:get("dog") 如果返回值 res 是 nil,就说明函调用失败了;如果 res 是 ngx.null 就说明redis 中不存在 dog 这个key。 而在处理 cjson 的时候,又有cjson.null这个值。 所以还是要根据对应库的文档来做空值的判断和区分。 在写类似 if not res then 这样的代码的时候,要特别留意下,最好改成明确的: if res ~= nil and res ~= false then ,类似这样的,并有对应的测试案例覆盖。

    1
  • HelloBug
    2019-06-15
    温铭老师,你好~ 在“为什么选择luaJIT”的讲解中,讲到如果lua函数入口或者lua循环足够热,JIT编译器会从热函数的入口或者热循环的某个位置开始尝试编译。有以下几个问题请教一下老师。 1.这里的“尝试”编译的意思是并不是所有的热函数或者热循环都可以被编译,是吧? 2.另一个疑问是为什么一开始不对所有的可以被JIT编译的函数或者循环直接进行编译呢? 3.现在的JIT发现热点然后进行编译,为什么判断是否是热点时的统计,是根据一个随机的阈值,而不是一个其他,比如经过测试认为比较合理的值?
    展开

    作者回复: LuaJIT 是会记录并统计代码的运行次数,才能知道哪些是热函数和热循环,这就是“尝试”的含义; 如果有些函数只调用了一两次,就没有编译的必要性了; 第三个问题,我的猜测可能是处于安全或者类似的考虑,就没有写死一个值,而是随机。

    共 2 条评论
    1
  • shineyyl
    2019-06-12
    $ resty -e 'local t; t={100}; ngx.say(t[1]);print(type(t[1]))' 100 number $ resty -e 'local t; t={100}; ngx.say(t[0]);print(type(t[0]))' #空 nil #nil,需要print打印出来t的数据类型
    展开
  • 一步
    2019-06-12
    老师 文中说了 序列不应该含有 nil的,array 是可以包含 nil
    共 1 条评论