178 lines
10 KiB
Markdown
178 lines
10 KiB
Markdown
|
||
|
||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||
|
||
|
||
08 LuaJIT分支和标准Lua有什么不同?
|
||
你好,我是温铭。
|
||
|
||
这节课,我们来学习下 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.new 和 table.clear。这是两个在性能优化方面非常重要的函数,在 OpenResty 的 lua-resty 库中会被频繁使用。不过,由于相关文档藏得非常深,而且没有示例代码,所以熟悉它们的开发者并不多。我们留到性能优化章节专门来讲它们。
|
||
|
||
写在最后
|
||
|
||
让我们来回顾下今天的内容。
|
||
|
||
OpenResty 出于性能的考虑,选择了 LuaJIT 而不是标准 Lua,并且维护了自己的 LuaJIT 分支。而 LuaJIT 基于 Lua 5.1 的语法,并选择性地兼容了部分 Lua5.2 和 Lua5.3 的语法,形成了自己的体系。至于你需要掌握的Lua 语法,在下标、字符串拼接、数据结构和变量上,都有自己鲜明的特点,在写代码的时候你应该特别留意。
|
||
|
||
你在学习 Lua 和 LuaJIT 的时候,是否遇到一些陷阱和坑呢?欢迎留言一起来聊一聊,我在后面也专门写了一篇文章,来分享我遇到过的那些坑。也欢迎你把这篇文章分享给你的同事、朋友,一起学习,一起进步。
|
||
|
||
|
||
|
||
|