[趣聞] 代碼順序也會影響 LuaJIT 的執行效率

最近有一個有趣的發現,調整了一行 Lua 代碼的順序,執行時間卻少了接近一半 😅segmentfault

現場案例

狀況下面這個 lua 腳本 order-1.lua函數

local function f2 (...)
    return select('#', ...)
end

local function f1 (...)
    local l = select('#', ...)
    local m = 0
    for i = 1, l do
        m = m + select(i, ...)
    end
    
    local n = f2(...)

    return m + n
end

local n = 0
for i = 1, 1000 * 1000 * 100 do
    n = n + f1(1, 2, 3, 4, 5)
end

print("n: ", n)

執行時間爲 6.3s性能

$ time luajit order-1.lua
n:      2000000000

real    0m6.343s
user    0m6.342s
sys     0m0.000s

若是將其中的 f1 函數實現,調整一下順序:學習

local function f1 (...)
    local n = f2(...)

    local l = select('#', ...)
    local m = 0
    for i = 1, l do
        m = m + select(i, ...)
    end
    
    return m + n
end

這個改動是將 n 的計算放到 m 計算的前面。
從邏輯上來講,mn 兩個是並無順序依賴,先算哪個都同樣的,可是執行時間卻少了將近一半:lua

$ time luajit order-2.lua
n:      2000000000

real    0m3.314s
user    0m3.312s
sys     0m0.002s

緣由分析

首先確定不是什麼詭異問題,計算機但是人類最真實的夥伴了,哈哈 😄日誌

此次是 Lua 這種高級語言,也不是 上次那種 CPU 指令級 的影響了。code

tracing JIT

此次是由於 LuaJIT 的 tracing JIT 技術的影響。ci

不像 Java 那種 method based JIT 技術,是按照函數來即時編譯的。LuaJIT 是按照 trace 來即時編譯的,trace 對應的是一串代碼執行路徑。
LuaJIT 會把熱的代碼路徑直接即時編譯生成機器碼,一串熱的代碼路徑也就是一個 trace。同時 trace 也不是無限長的,LuaJIT 有一套機制來控制 trace 的開始結束(之後找時間再詳細記錄一篇的)。get

具體來講是這樣子的,由於在 order-1.lua 裏,TRACE 1m 計算的那個 for 循環處則中止了,當 TRACE 2 開始的時候,LuaJIT 還不支持這種狀況下即時編譯 (還處於 NYI 狀態)VARG 這個字節碼(也就是對應的 ...)。it

因此,致使了這部分代碼不能被 JIT,迴歸到了 interpreter 模式,因此致使了這麼大的性能差別。

以下,咱們能夠在 LuaJIT 輸的日誌中看到 NYI: bytecode 71 這個關鍵信息。

$ luajit -jdump=bitmsr order-1.lua

...

---- TRACE 2 start 1/3 order.lua:13
0016  UGET     2   0      ; f2       (order.lua:13)
0017  VARG     4   0   0       (order.lua:13)
---- TRACE 2 abort order.lua:13 -- NYI: bytecode 71

總結

調整了 Lua 代碼順序,影響了 LuaJIT 中 trace 的生成,致使了有字節碼無法被 JIT,這部分回退到了解釋模式,從而致使了較大的性能差別。

感慨一下

JIT 技術仍是蠻好玩,不過須要學習掌握的東西也挺多的。

以我目前的理解,tracing JIT 算是很牛的 JIT 技術了,有其明顯的優點。不過任何一項技術,老是少不了很是多的人力投入。即便像 Lua 這種小巧的語言,也仍是有很多的 NYI 沒有被 JIT 技術。像 Java 這種重型語言,JIT 這方面的技術,怕是須要不少大牛才堆出來的。

相關文章
相關標籤/搜索