W3Cschool
恭喜您成為首批注冊用戶
獲得88經(jīng)驗(yàn)值獎(jiǎng)勵(lì)
自從 OpenResty 1.5.8.1 版本之后,默認(rèn)捆綁的 Lua 解釋器就被替換成了 LuaJIT,而不再是標(biāo)準(zhǔn) Lua。單從名字上,我們就可以直接看到這個(gè)新的解釋器多了一個(gè) ?JIT
?,接下來我們就一起來聊聊 ?JIT
?。
先看一下 LuaJIT 官方的解釋:LuaJIT is a Just-In-Time Compilerfor the Lua programming language。
LuaJIT 的運(yùn)行時(shí)環(huán)境包括一個(gè)用手寫匯編實(shí)現(xiàn)的 Lua 解釋器和一個(gè)可以直接生成機(jī)器代碼的 JIT 編譯器。
Lua 代碼在被執(zhí)行之前總是會先被 lfn 成 LuaJIT 自己定義的字節(jié)碼(Byte Code)。關(guān)于 LuaJIT 字節(jié)碼的文檔,可以參見:http://wiki.luajit.org/Bytecode-2.0(這個(gè)文檔描述的是 LuaJIT 2.0 的字節(jié)碼,不過 2.1 里面的變化并不算太大)。
一開始的時(shí)候,Lua 字節(jié)碼總是被 LuaJIT 的解釋器解釋執(zhí)行。LuaJIT 的解釋器會在執(zhí)行字節(jié)碼時(shí)同時(shí)記錄一些運(yùn)行時(shí)的統(tǒng)計(jì)信息,比如每個(gè) Lua 函數(shù)調(diào)用入口的實(shí)際運(yùn)行次數(shù),還有每個(gè) Lua 循環(huán)的實(shí)際執(zhí)行次數(shù)。當(dāng)這些次數(shù)超過某個(gè)預(yù)設(shè)的閾值時(shí),便認(rèn)為對應(yīng)的 Lua 函數(shù)入口或者對應(yīng)的 Lua 循環(huán)足夠的“熱”,這時(shí)便會觸發(fā) JIT 編譯器開始工作。
JIT 編譯器會從熱函數(shù)的入口或者熱循環(huán)的某個(gè)位置開始嘗試編譯對應(yīng)的 Lua 代碼路徑。編譯的過程是把 LuaJIT 字節(jié)碼先轉(zhuǎn)換成 LuaJIT 自己定義的中間碼(IR),然后再生成針對目標(biāo)體系結(jié)構(gòu)的機(jī)器碼(比如 x86_64 指令組成的機(jī)器碼)。
如果當(dāng)前 Lua 代碼路徑上的所有的操作都可以被 JIT 編譯器順利編譯,則這條編譯過的代碼路徑便被稱為一個(gè)“trace”,在物理上對應(yīng)一個(gè) ?trace
?類型的 GC 對象(即參與 Lua GC 的對象)。
你可以通過 ?ngx-lj-gc-objs
? 工具看到指定的 Nginx worker 進(jìn)程里所有 ?trace
? 對象的一些基本的統(tǒng)計(jì)信息,見 https://github.com/openresty/stapxx#ngx-lj-gc-objs
比如下面這一行 ?ngx-lj-gc-objs
? 工具的輸出
102 trace objects: max=928, avg=337, min=160, sum=34468 (in bytes)
則表明當(dāng)前進(jìn)程內(nèi)的 LuaJIT VM 里一共有 102 個(gè) trace 類型的 GC 對 象,其中最小的 trace 占用 160 個(gè)字節(jié),最大的占用 928 個(gè)字節(jié),平均大小是 337 字節(jié),而所有 trace 的總大小是 34468 個(gè)字節(jié)。
LuaJIT 的 JIT 編譯器的實(shí)現(xiàn)目前還不完整,有一些基本原語它還無法編譯,比如 pairs() 函數(shù)、unpack() 函數(shù)、string.match() 函數(shù)、基于 lua_CFunction 實(shí)現(xiàn)的 Lua C 模塊、FNEW 字節(jié)碼,等等。所以當(dāng) JIT 編譯器在當(dāng)前代碼路徑上遇到了它不支持的操作,便會立即終止當(dāng)前的 trace 編譯過程(這被稱為 trace abort),而重新退回到解釋器模式。
JIT 編譯器不支持的原語被稱為 NYI(Not Yet Implemented)原語。比較完整的 NYI 列表在這篇文檔里面:
http://wiki.luajit.org/NYI
所謂“讓更多的 Lua 代碼被 JIT 編譯”,其實(shí)就是幫助更多的 Lua 代碼路徑能為 JIT 編譯器所接受。這一般通過兩種途徑來實(shí)現(xiàn):
對于第 2 種方式,春哥一直在推動公司(CloudFlare)贊助 Mike Pall 的開發(fā)工作。不過有些原語因?yàn)楸旧淼拇鷥r(jià)過高,而永遠(yuǎn)不會被編譯,比如基于經(jīng)典的 lua_CFunction 方式實(shí)現(xiàn)的 Lua C 模塊(所以需要盡量通過 LuaJIT 的 FFI 來調(diào)用 C)。
而對于第 1 種方法,我們?nèi)绾尾拍苤谰唧w是哪一行 Lua 代碼上的哪一個(gè) NYI 原語終止了 trace 編譯呢?答案很簡單。就是使用 LuaJIT 安裝自帶的 jit.v 和 jit.dump 這兩個(gè) Lua 模塊。這兩個(gè) Lua 模塊會打印出 JIT 編譯器工作的細(xì)節(jié)過程。
在 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 代碼中,當(dāng) verbose 變量為 false 時(shí)(默認(rèn)就為 false 哈),我們使用 jit.v 模塊打印出比較簡略的流水信息到 /tmp/jit.log 文件中;而當(dāng) verbose 變量為 true 時(shí),我們則使用 jit.dump 模塊打印所有的細(xì)節(jié)信息,包括每個(gè) trace 內(nèi)部的字節(jié)碼、IR 碼和最終生成的機(jī)器指令。
這里我們主要以 jit.v 模塊為例。在啟動 Nginx 之后,應(yīng)當(dāng)使用 ab 和 weighttp 這樣的工具對相應(yīng)的服務(wù)接口進(jìn)行預(yù)熱,以觸發(fā) LuaJIT 的 JIT 編譯器開始工作(還記得剛才我們說的“熱函數(shù)”和“熱循環(huán)”嗎?)。預(yù)熱過程一般不用太久,跑個(gè)二三百個(gè)請求足矣。當(dāng)然,壓更多的請求也沒關(guān)系。完事后,我們就可以檢查 /tmp/jit.log 文件里面的輸出了。
jit.v 模塊的輸出里如果有類似下面這種帶編號的 TRACE 行,則指示成功編譯了的 trace 對象,例如
[TRACE 6 shdict.lua:126 return]
這個(gè) trace 對象編號為 6,對應(yīng)的 Lua 代碼路徑是從 shdict.lua 文件的第 126 行開始的。
下面這樣的也是成功編譯了的 trace:
[TRACE 16 (15/1) waf-core.lua:419 -> 15]
這個(gè) trace 編號為 16,是從 waf-core.lua 文件的第 419 行開始的,同時(shí)它和編號為 15 的 trace 聯(lián)接了起來。
而下面這個(gè)例子則是被中斷的 trace:
[TRACE --- waf-core.lua:455 -- NYI: FastFunc pairs at waf-core.lua:458]
上面這一行是說,這個(gè) trace 是從 waf-core.lua 文件的第 455 行開始編譯的,但當(dāng)編譯到 waf-core.lua 文件的第 458 行時(shí),遇到了一個(gè) NYI 原語編譯不了,即 pairs() 這個(gè)內(nèi)建函數(shù),于是當(dāng)前的 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]
上面第二行是因?yàn)椴僮鞔a 51 的 LuaJIT 字節(jié)碼也是 NYI 原語,編譯不了。
那么我們?nèi)绾沃?51 字節(jié)碼究竟是啥呢?我們可以用 nginx-devel-utils 項(xiàng)目中的 ljbc.lua 腳本來取得 51 號字節(jié)碼的名字:
$ /usr/local/openresty/luajit/bin/luajit-2.1.0-alpha ljbc.lua 51
opcode 51:
FNEW
我們看到原來是用來(動態(tài))創(chuàng)建 Lua 函數(shù)的 FNEW 字節(jié)碼。ljbc.lua 腳本的位置是
https://github.com/agentzh/nginx-devel-utils/blob/master/ljbc.lua
非常簡單的一個(gè)腳本,就幾行 Lua 代碼。
這里需要提醒的是,不同版本的 LuaJIT 的字節(jié)碼可能是不相同的,所以一定要使用和你的 Nginx 鏈接的同一個(gè) LuaJIT 來運(yùn)行這個(gè) ljbc.lua 工具,否則有可能會得到錯(cuò)誤的結(jié)果。
我們實(shí)際做個(gè)對比實(shí)驗(yàn),看看 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 倍,換句話說標(biāo)準(zhǔn) Lua 需要 177% 的時(shí)間才能完成同樣的工作。估計(jì)大家覺得這個(gè)還不過癮,再看下面示例代碼:
文件 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
執(zhí)行參數(shù) | 執(zhí)行結(jié)果 |
---|---|
time lua test.lua 1000 ipairs | 3.96s user 0.02s system 98% cpu 4.039 total |
time lua test.lua 1000 pairs | 3.97s user 0.01s system 99% cpu 3.992 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 |
time luajit test.lua 1000 pairs | 1.54s user 0.01s system 99% cpu 1.559 total |
從這個(gè)執(zhí)行結(jié)果中,大致可以總結(jié)出下面幾個(gè)觀點(diǎn):
下面給大家列一下截止到目前已經(jīng)可以被 JIT 編譯的元操作。 其他還有 IO、Bit、FFI、Coroutine、OS、Package、Debug、JIT 等分類,使用頻率相對較低,這里就不羅列了,可以參考官網(wǎng):http://wiki.luajit.org/NYI。
函數(shù) | 編譯? | 備注 |
---|---|---|
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 | |
no | ||
rawequal | yes | |
rawget | yes | |
rawlen (5.2) | yes | |
rawset | yes | |
select | partial | 第一個(gè)參數(shù)是靜態(tài)變量的時(shí)候可以編譯 |
setfenv | no | |
setmetatable | yes | |
tonumber | partial | 不能編譯非10進(jìn)制,非預(yù)期的異常輸入 |
tostring | partial | 只能編譯:字符串、數(shù)字、布爾、nil 以及支持 __tostring元方法的類型 |
type | yes | |
unpack | no | |
xpcall | yes |
函數(shù) | 編譯? | 備注 |
---|---|---|
string.byte | yes | |
string.char | 2.1 | |
string.dump | never | |
string.find | 2.1 partial | 只有字符串樣式查找(沒有樣式) |
string.format | 2.1 partial | 不支持 %p 或 非字符串參數(shù)的 %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 |
函數(shù) | 編譯? | 備注 |
---|---|---|
table.concat | 2.1 | |
table.foreach | no | 2.1: 內(nèi)部編譯,但還沒有外放 |
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 |
函數(shù) | 編譯? | 備注 |
---|---|---|
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 |
Copyright©2021 w3cschool編程獅|閩ICP備15016281號-3|閩公網(wǎng)安備35020302033924號
違法和不良信息舉報(bào)電話:173-0602-2364|舉報(bào)郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號
聯(lián)系方式:
更多建議: