觀察者:Lua JIT詳解

觀察者:Lua JIT詳解

自從 OpenResty 1.5.8.1 版本之後,默認捆綁的 Lua 解釋器就被替換成了 LuaJIT,而不再是標準 Lua。單從名字上,我們就可以直接看到這個新的解釋器多了一個 JIT ,接下來我們就一起來聊聊 JIT 。

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

LuaJIT 的運行時環境包括一個用手寫彙編實現的 Lua 解釋器和一個可以直接生成機器代碼的JIT 編譯器。

Lua 代碼在被執行之前總是會先被 lfn 成 LuaJIT 自己定義的字節碼(Byte Code) 。

一開始的時候,Lua 字節碼總是被 LuaJIT 的解釋器解釋執行。LuaJIT 的解釋器會在執行字節碼時同時記錄一些運行時的統計信息,比如每個 Lua 函數調用入口的實際運行次數,還有每個 Lua 循環的實際執行次數。當這些次數超過某個預設的閾值時,便認為對應的 Lua 函數入口或者對應的 Lua 循環足夠的“熱”,這時便會觸發 JIT 編譯器開始工作。

JIT 編譯器會從熱函數的入口或者熱循環的某個位置開始嘗試編譯對應的 Lua 代碼路徑。編譯的過程是把 LuaJIT 字節碼先轉換成LuaJIT 自己定義的中間碼(IR) ,然後再生成針對目標體系結構的機器碼(比如 x86_64 指令組成的機器碼) 。

如果當前 Lua 代碼路徑上的所有的操作都可以被 JIT 編譯器順利編譯,則這條編譯過的代碼路徑便被稱為一個“trace”,在物理上對應一個 trace 類型的 GC 對象(即參與 Lua GC 的對象) 。

你可以通過 ngx-lj-gc-objs 工具看到指定的 Nginx worker 進程裡所有 trace 對象的一些基本的統計信息,

比如下面這一行 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 個字節。


LuaJIT 的 JIT 編譯器的實現目前還不完整,有一些基本原語它還無法編譯,比如 pairs() 函數、unpack() 函數、string.match() 函數、基於 lua_CFunction 實現的 Lua C 模塊、FNEW字節碼,等等。所以當 JIT 編譯器在當前代碼路徑上遇到了它不支持的操作,便會立即終止當前的 trace 編譯過程(這被稱為 trace abort) ,而重新退回到解釋器模式。

JIT 編譯器不支持的原語被稱為 NYI(Not Yet Implemented) 原語。

所謂“讓更多的 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 編譯器工作的細節過程。

在 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 對象,例如

[TRACE 6 shdict.lua:126 return]

這個 trace 對象編號為 6,對應的 Lua 代碼路徑是從 shdict.lua 文件的第 126 行開始的。

下面這樣的也是成功編譯了的 trace:

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

這個 trace 編號為 16,是從 waf-core.lua 文件的第 419 行開始的,同時它和編號為 15 的trace 聯接了起來。

而下面這個例子則是被中斷的 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 原語,編譯不了。

那麼我們如何知道 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 工具,否則有可能會得到錯誤的結果。

我們實際做個對比實驗,看看 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

執行參數執行結果

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

從這個執行結果中,大致可以總結出下面幾個觀點:

在標準 Lua 解釋器中,使用 ipairs 或 pairs 沒有區別;

對於 pairs 方式,LuaJIT 的性能大約是標準 Lua 的 4 倍;

對於 ipairs 方式,LuaJIT 的性能大約是標準 Lua 的 40 倍。

可以被 JIT 編譯的元操作

下面給大家列一下截止到目前已經可以被 JIT 編譯的元操作。 其他還有 IO、Bit、FFI、Coroutine、OS、Package、Debug、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


分享到:


相關文章: