Lua腳本是C語(yǔ)言實(shí)現(xiàn)的腳本署海,廣泛應(yīng)用于客戶端擴(kuò)展腳本采够,例如魔獸世界等網(wǎng)游肄方。但是Lua的性能一般,并且有許多不好的實(shí)現(xiàn)蹬癌,誤用會(huì)大大降低系統(tǒng)的性能权她。
網(wǎng)絡(luò)上有一些關(guān)于Lua腳本性能優(yōu)化的資料,但是都是針對(duì)Lua撰寫的逝薪,寫作年代較早隅要,一些優(yōu)化技巧不完全正確,而且沒有針對(duì)LuaJIT優(yōu)化過后的代碼進(jìn)行考慮董济。
本章對(duì)于Lua的一些語(yǔ)法步清,在Lua和LuaJIT中進(jìn)行比較測(cè)試,并給出相關(guān)優(yōu)化數(shù)據(jù)和結(jié)論虏肾。
由于LuaJIT的性能較Lua有很大的提高尼啡,在測(cè)試時(shí)使用的循環(huán)次數(shù)不同暂衡,以避免時(shí)間太短導(dǎo)致測(cè)量不準(zhǔn)確。
在結(jié)果中崖瞭,In LuaJIT (100x)
表示LuaJIT中執(zhí)行性能但愿測(cè)試次數(shù)是Lua中的100倍狂巢。
相關(guān)參考資料:
- Roberto Ierusalimschy. Lua Performance Tips. http://www.lua.org/gems/sample.pdf
- Lua Performance: http://springrts.com/wiki/Lua_Performance
目錄
變量局部化
Lua變量區(qū)分為全局變量和局部變量。Lua為每一個(gè)函數(shù)分配了一套多達(dá)250個(gè)的寄存器书聚,并用這些寄存器存儲(chǔ)局部變量唧领,這使得Lua中局部變量的訪問速度很快。
相反的是雌续,對(duì)于全局變量斩个,Lua需要將全局變量讀出存入當(dāng)前函數(shù)的寄存器中,然后完成計(jì)算之后再存回全局變量表中驯杜。
這樣受啥,一個(gè)類似a = a + b
這樣的簡(jiǎn)單計(jì)算,若a
和b
為局部變量鸽心,則編譯之后只生成一條Lua指令滚局,而若為全局變量,則生成4條Lua指令顽频。Lua Performance Tips中提到藤肢,將全局變量變?yōu)榫植孔兞浚缓笤谑褂眠M(jìn)行訪問糯景,速度可以提升約30%嘁圈,編寫如下代碼進(jìn)行實(shí)驗(yàn):
代碼和結(jié)果
function nonlocal()
local x = 0
for i = 1, 100 do
x = x + math.sin(i)
end
return x
end
local sin = math.sin
function localized()
local x = 0
for i = 1, 100 do
x = x + sin(i)
end
return x
end
--[[------------------------
In Lua (1x)
ID Case name t1 t2 t3 t4 t5 avg. %
1 Non-local 1.66 1.65 1.65 1.66 1.66 1.656 100%
2 Localized 1.25 1.25 1.25 1.25 1.25 1.25 75.48%
In LuaJIT (2x)
ID Case name t1 t2 t3 t4 t5 avg. %
1 Non-local 1.3 1.31 1.3 1.3 1.3 1.302 100%
2 Localized 1.3 1.3 1.3 1.29 1.3 1.298 99.69%
--]]------------------------
結(jié)論
在普通Lua的解釋下,行為和//Lua Performance Tips//中所述大致一致蟀淮,調(diào)用全局變量中的函數(shù)大約會(huì)慢30%最住。
而在LuaJIT的解釋下,兩者差異不明顯怠惶。
不是一定需要將全局變量中的變量轉(zhuǎn)變?yōu)榫植孔兞俊?/p>
多重條件判斷
在Lua中温学,唯一的數(shù)據(jù)結(jié)構(gòu)table是哈希表,創(chuàng)建甚疟、銷毀和迭代都需要?jiǎng)?chuàng)建很多資源仗岖。
本例對(duì)比當(dāng)判斷較多條件時(shí),使用連續(xù)邏輯表達(dá)式和使用table的性能览妖。
代碼和結(jié)果
function use_or()
local x = 0
for _, v in pairs(states) do
if "alabama" == v or
"california" == v or
"missouri" == v or
"virginia" == v or
"wisconsin" == v then
x = x + 1
end
end
return x
end
function use_table()
local x, vals = 0, {"alabama", "california", "missouri", "virginia", "wisconsin"}
for _, v in pairs(states) do
for _, s in pairs(vals) do
if s == v then x = x + 1 end
end
end
return x
end
--[[------------------------
In Lua (1x)
ID Case name t1 t2 t3 t4 t5 avg. %
1 Use OR 0.39 0.38 0.39 0.39 0.38 0.386 100%
2 Use table 1.85 1.84 1.84 1.85 1.84 1.844 477.72%
In LuaJIT (10x)
ID Case name t1 t2 t3 t4 t5 avg. %
1 Use OR 0.6 0.6 0.59 0.59 0.6 0.596 100%
2 Use table 2.82 2.85 2.86 2.86 2.85 2.848 477.85%
--]]------------------------
結(jié)論
當(dāng)判斷較多邏輯條件時(shí)轧拄,應(yīng)當(dāng)使用簡(jiǎn)單的邏輯運(yùn)算,而table創(chuàng)建讽膏、銷毀和迭代的開銷較大檩电,應(yīng)避免使用。
盡管使用循環(huán)的方法,程序可能具有較好的可讀性俐末,但是性能會(huì)嚴(yán)重下降料按。
使用邏輯表達(dá)式判斷,采取較好的寫法也可以獲得可讀性卓箫。
函數(shù)調(diào)用
函數(shù)是我們對(duì)功能進(jìn)行封裝的基本方法载矿,但是函數(shù)的調(diào)用也是具有一定的開銷,本例反映了函數(shù)調(diào)用的時(shí)間開銷問題烹卒。
其中闷盔,直接計(jì)算部分在測(cè)試函數(shù)中內(nèi)嵌代碼進(jìn)行階乘計(jì)算,而函數(shù)調(diào)用部分則使用另外封裝的階乘計(jì)算函數(shù)旅急。
代碼和結(jié)果
function direct_compute()
local x = 0
for i = 1, 30 do
local r = 1
for j = 1, i do
r = r * j
end
x = x + r
end
return x
end
function fact(x)
local result = 1
for i = 1, x do
result = result * i
end
return result
end
function call_function()
local x = 0
for i = 1, 30 do
x = x + fact(i)
end
return x
end
--[[------------------------
In Lua (1x)
ID Case name t1 t2 t3 t4 t5 avg. %
1 Direct compute 1.17 1.17 1.17 1.17 1.18 1.172 100%
2 Call function 1.5 1.5 1.51 1.51 1.5 1.504 128.33%
In LuaJIT (10x)
ID Case name t1 t2 t3 t4 t5 avg. %
1 Direct compute 1.08 1.08 1.08 1.08 1.07 1.078 100%
2 Call function 1.32 1.32 1.33 1.32 1.32 1.322 122.63%
--]]------------------------
結(jié)論
使用函數(shù)封裝之后逢勾,消耗的時(shí)間Lua中增加28%,LuaJIT中增加22%藐吮。如果是會(huì)反復(fù)調(diào)用的功能溺拱,如無必要,如代碼復(fù)用等問題谣辞,則應(yīng)當(dāng)盡可能避免多次調(diào)用函數(shù)迫摔。
尾遞歸
在一般的編程語(yǔ)言中,函數(shù)遞歸會(huì)占用椓氏校空間攒菠,層次很深的遞歸容易導(dǎo)致棧溢出迫皱。
在一些編程語(yǔ)言歉闰,尤其是函數(shù)式編程語(yǔ)言中,對(duì)一種特殊的遞歸方法——尾遞歸進(jìn)行了優(yōu)化卓起。
Lua中也是如此和敬,使得尾遞歸的函數(shù)在遞歸開始時(shí)會(huì)釋放當(dāng)前函數(shù)的調(diào)用棧空間戏阅,從而保證多級(jí)遞歸也不會(huì)額外占用椫绲埽空間。
本例中展示尾遞歸的性能奕筐,使用普通遞歸舱痘、普通循環(huán)和尾遞歸三種方法編寫計(jì)算階乘的程序,并以普通遞歸方法作的數(shù)據(jù)為對(duì)比的基準(zhǔn)离赫。
代碼和結(jié)果
-- 普通遞歸
function fact_recurse(x)
if x <= 1 then return 1 end
return x * fact_recurse(x - 1)
end
-- 普通循環(huán)
function fact_normal(x)
local result = 1
for i = 2, x do
result = result * i
end
return result
end
-- 尾遞歸
function fact_tail(x, r)
local result = r or 1
if x <= 1 then return result end
return fact_tail(x - 1, result * x)
end
--[[------------------------
In Lua (1x)
ID Case name t1 t2 t3 t4 t5 avg. %
1 Normal recurse 1.88 1.88 1.89 1.88 1.88 1.882 100%
2 Normal loop 0.5 0.49 0.5 0.5 0.49 0.496 26.35%
3 Tail recurse 1.99 2 1.99 1.99 1.99 1.992 105.84%
In LuaJIT (20x)
ID Case name t1 t2 t3 t4 t5 avg. %
1 Normal recurse 3.02 3 3 3 2.99 3.002 100%
2 Normal loop 0.98 0.97 0.97 0.97 0.98 0.974 32.45%
3 Tail recurse 1.06 1.07 1.07 1.07 1.07 1.068 35.58%
--]]------------------------
結(jié)論
雖然Lua中強(qiáng)調(diào)了對(duì)尾遞歸的優(yōu)化芭逝,但是實(shí)際執(zhí)行結(jié)果表明,Lua中的尾遞歸只是對(duì)椩ㄐ兀空間的分配進(jìn)行了優(yōu)化旬盯,速度并沒有比普通遞歸有提升。
而在LuaJIT中,使用尾遞歸編寫的函數(shù)具有與普通循環(huán)相接近的性能胖翰。
不過尾遞歸函數(shù)不太容易編寫接剩,在實(shí)際使用過程中可以使用。
Table
table追加
Lua的標(biāo)準(zhǔn)庫(kù)函數(shù)中萨咳,并不是所有函數(shù)都實(shí)現(xiàn)得很好懊缺,尤其是table數(shù)據(jù)結(jié)構(gòu)的實(shí)現(xiàn)性能較差,table.insert
函數(shù)就是一個(gè)性能較低的函數(shù)某弦。
代碼和結(jié)果
function table_insert()
local t = {}
for i = 1, 1000, 2 do
table.insert(t, i)
end
return t
end
function table_insertL()
local t, insert = {}, table.insert
for i = 1, 1000, 2 do
insert(t, i)
end
return t
end
function use_counter()
local t, c = {}, 1
for i = 1, 1000, 2 do
t[c], c = i, c + 1
end
return t
end
function use_length()
local t = {}
for i = 1, 1000, 2 do
t[#t + 1] = i
end
end
--[[------------------------
In Lua (1x)
ID Case name t1 t2 t3 t4 t5 avg. %
1 table.insert G 2.72 2.73 2.72 2.73 2.72 2.724 100%
2 table.insert L 2.24 2.24 2.24 2.24 2.24 2.24 82.23%
3 use counter 1.13 1.13 1.14 1.12 1.13 1.13 41.48%
4 use length 1.43 1.44 1.43 1.43 1.43 1.432 52.57%
In LuaJIT (5x)
ID Case name t1 t2 t3 t4 t5 avg. %
1 table.insert G 3.69 3.69 3.68 3.68 3.69 3.686 100%
2 table.insert L 3.75 3.75 3.75 3.75 3.74 3.748 101.68%
3 use counter 0.86 0.85 0.85 0.86 0.85 0.854 23.17%
4 use length 3.71 3.71 3.71 3.71 3.71 3.71 100.65%
--]]------------------------
結(jié)論
當(dāng)需要生成一個(gè)數(shù)組桐汤,并往數(shù)組的尾部添加數(shù)據(jù)時(shí),應(yīng)當(dāng)盡可能的使用計(jì)數(shù)器靶壮,如果沒辦法使用計(jì)數(shù)器怔毛,也應(yīng)當(dāng)使用#
運(yùn)算符先求出數(shù)組的長(zhǎng)度,然后使用計(jì)數(shù)器插入數(shù)組腾降。
轉(zhuǎn):https://github.com/flily/lua-performance/blob/master/Guide.zh.md