幾種Lua優(yōu)化技巧
Lua優(yōu)化技巧選講
內(nèi)存優(yōu)化
眾所周知,Lua使用的是糟糕程度僅次于“手動分配堆內(nèi)存、指針亂飄”的內(nèi)存模型——GC模型。這意味著,對于Table、String等復(fù)雜類型,只要你搞丟了相應(yīng)的引用名,對應(yīng)的東西就會徹底變成垃圾一團(tuán),并且不會自動釋放,也無法手動要求釋放,只能等著垃圾回收器去回收了。
舉一個簡單的栗子AwA
a = {x = 100,
? ?y = 200}
a = {x = 300,
? ?y = 400}
這里實際上我們構(gòu)造了兩個Table,然后第一個Table的引用(一開始是a)就這么被我們搞丟了,然后第一個Table就變成垃圾了。
一般情況下隨意點也沒啥大的問題,但我們在Defold做的游戲一般要跑60fps,一些音游甚至跑120fps,考慮這樣一個情境:
function update(self,dt)
? ?for k=1,60 do
? ? ? ?a = {
? ? ? ? ? ?x = k-1,
? ? ? ? ? ?y = k+1
? ? ? ?}
? ? ? ?b = {
? ? ? ? ? ?x = k+10,
? ? ? ? ? ?y = k-10
? ? ? ?}
? ? ? ?self.my_x += a.x*b.y
? ? ? ?self.my_y += a.y*b.x
? ?end
end
一秒跑完那60幀,就制造了7200個表的垃圾,這下非常可觀了。
因此,多復(fù)用現(xiàn)有的表:
local a = {
??????x = 0,
????? y = 0
??????}
local b = {
? ????x = 0,
???? ?y = 0
???? ?}
function update(self,dt)
? ?for k=1,60 do
? ? ? ?a.x = k-1
? ? ? ?a.y = k+1
? ? ? ?b.x = k+10
? ? ? ?b.y = k-10
? ?end
? ?self.my_x += a.x*b.y
? ?self.my_y += a.y*b.x
end
這下update一個表都不會造了www
另外,大家提得比較多的就是String不可變的問題,先給個不好的例子:
names = {"甲","乙","丙","丁","戊"}
ages = {19,21,20,19,18}
position = "中南大學(xué)"
for i,value in ipairs(names) do
? ?print(value .. "," .. ages[i] .. "歲,現(xiàn)就職于" .. position)
end
-- 甲,19歲,現(xiàn)就職于中南大學(xué)
-- 乙,21歲,現(xiàn)就職于中南大學(xué)
-- 丙,20歲,現(xiàn)就職于中南大學(xué)
-- 丁,19歲,現(xiàn)就職于中南大學(xué)
-- 戊,18歲,現(xiàn)就職于中南大學(xué)
為了打出這5句狗話,系統(tǒng)額外申請了30塊堆區(qū)內(nèi)存用來存放拼接過程。。。。。。
然后這里有個好點的例子:
names = {"甲","乙","丙","丁","戊"}
ages = {19,21,20,19,18}
position = "中南大學(xué)"
pre_str = "%s,%d歲,現(xiàn)就職于%s"
for i=1,#names do
? ?local concat = string.format(pre_str, names[i], ages[i], position)
? ?print(concat)
end
現(xiàn)在我們引入print( collectgarbage("count") )
統(tǒng)計下內(nèi)存消耗:

差距還是挺大的,嗯。
最后,Lua非常友善地提供了讓GC間歇運行的方法,考慮到游戲引擎往往提供更新函數(shù),潔癖比較深重的人推薦試試:
function init(self)
? ?collectgarbage("stop") ?--關(guān)閉GC觸發(fā)
? ?-- 然后干點別的
end
function update(self,dt)
? ?collectgarbage("step",4) ?--每幀回收相當(dāng)于4KB內(nèi)存的垃圾
? ?-- 這里干點別的
end
最后的最后……
Lua如果像上面一樣單步跑GC的話,可以借助協(xié)程把GC任務(wù)剝離到單獨的線程,但這樣做意義不大;如果是正常的倍率步進(jìn)GC的話,讓GC單獨占一個線程需要魔改Lua的本體——有點超過我們的討論范圍了,抱歉。
運行效率優(yōu)化
首先是緩存這件事,門道很深,先從Local部分講起。
有關(guān)Lua的一切,都是圍繞著“Table”這樣一種數(shù)據(jù)結(jié)構(gòu)展開的。舉例來說,為了允許把函數(shù)存儲在表內(nèi),Lua做了First-Class函數(shù),甚至閉包之類的支持。
我們直接在命令行打開Lua走交互式編程,寫點這種東西:
a = 2
b = 6
c = a*110 + b
print(c) ?--226
那么……這里的a、b、c在什么地方呢?答曰:儲存在全局表_G里。
Lua的Table是一種相當(dāng)之兼容并蓄的數(shù)據(jù)結(jié)構(gòu),而最常見的(非嵌套)Table大概長這兩種樣子:
hero = {
? ?hp = 100,
? ?mp = 100,
? ?weapon = "sword"
? ?}
main_subjects = {"Chinese","Math","English"}
前者對應(yīng)了一些語言的哈希表、字典,后者對應(yīng)一些動態(tài)語言的Array容器。
然后我們嘗試從這兩種Table里面拿點東西:
-- 書接上文
print(hero.hp)
?-- 100
print(main_subjects[1]) ?-- "Chinese"
取hero表里的hp字段時,Lua首先會把"hp"這么一串字符作哈希化處理,然后在描述“hero”這個表的Hash Map里逐個問詢其中的元素,看看指向哪塊內(nèi)存。
于是,執(zhí)行任何東西都無法避免這么一個查表輪詢的過程,相當(dāng)?shù)托?。因此,Lua的虛擬機提供了一種類似于CPU“寄存器”的東西,并且把它用local
這么一個關(guān)鍵字暴露了出來,順便為其增加了作用域相關(guān)的規(guī)則。
這也是為什么Lua的局部變量全都要顯式聲明。
就Defold引擎的Lua腳本而言,更新、輸入、消息函數(shù)調(diào)用頻度很高,對這些地方做Local緩存優(yōu)化效果會很明顯。
這里介紹一下個人比較傾向的優(yōu)化方式,先撿一段未經(jīng)優(yōu)化的腳本出來:
function on_message(self, message_id, message, sender)
? ?if message_id == hash("collision_response") then
? ? ? ?msg.post("main#gui", "add_score", {amount = score})
? ? ? ?particlefx.play("#pickup")
? ? ? ?go.delete()
? ?end
end
接著先做一下Local優(yōu)化,以及Table復(fù)用優(yōu)化:
local haxi = hash
local msg_post = msg.post
local particlefx_play = particlefx.play
local go_delete = go.delete
local scr_table = {amount = score}
function on_message(self, message_id, message, sender)
? ?if message_id == haxi("collision_response") then
? ? ? ?scr_table["amount"] = score
? ? ? ?msg_post("main#gui", "add_score", score)
? ? ? ?particlefx_play("#pickup")
? ? ? ?go_delete()
? ?end
end
可以看到,基本上都是體力活。。。。。。
就Defold而言,hash值、定位地址也是可以緩存的,這里進(jìn)一步對hash值作一次緩存:
local collision_response = hash("collision_response")
local msg_post = msg.post
local particlefx_play = particlefx.play
local go_delete = go.delete
local scr_table = {amount = score}
function on_message(self, message_id, message, sender)
? ?if message_id == collision_response then
? ? ? ?scr_table["amount"] = score
? ? ? ?msg_post("main#gui", "add_score", score)
? ? ? ?particlefx_play("#pickup")
? ? ? ?go_delete()
? ?end
end
String其實也可以預(yù)存一下,但是收益不大——因為Lua在識別到重復(fù)的String時,會自動轉(zhuǎn)換為對原有String的引用,并不存在同一String多次開辟內(nèi)存的行為。因此,預(yù)存String可以稍微提升一點點性能。
但就Defold采用的LuaJIT而言,Local變量過多可能會導(dǎo)致JIT放棄編譯,會有些得不償失,因此String預(yù)存的優(yōu)先級應(yīng)該靠后一些。
然后讓我們看向main_subjects[1]
部分。這個語句恰好就是main_subject這個表的第一個元素,針對這種情況應(yīng)該就沒有必要再開個Hash Map然后大找特找了。
Lua在設(shè)計時也考慮到了這一點。因此,Table在內(nèi)部其實區(qū)分成了Array Part和Hash Part,并且Array Part不需要查表的這個步驟、和數(shù)組更像一點。
因此,多使用Array Part;然后,尊重一下Array的有序性,盡量預(yù)先定好Array的大小不要輕易改變。
此外,LuaJIT提供了Array預(yù)分配大小的支持:
local table_new = require "table.new"
local my_new_array = table_new(100,0)
-- 新建一個Array部分預(yù)分配100個元素、Hash部分不預(yù)分配元素的Table
-- my_new_array是剛剛新建的Table的引用
但是Defold Script不能引入這個函數(shù),洗洗睡吧()
接下來讓我們談?wù)劚闅v。在LuaJIT的世界里,for i=1,#table
和for k,v in ipairs(table)
效果差不多,都遠(yuǎn)遠(yuǎn)優(yōu)于for k,v in pairs(table)
。原因無他,LuaJIT對pairs()
的JIT支持還是Not Yet Implemented。。。。。。
最后,不推薦硬寫OOP。Lua對OOP優(yōu)化遠(yuǎn)低于正經(jīng)OOP語言,建議選取更扁平的數(shù)據(jù)結(jié)構(gòu)。
同樣給個糟糕的例子:
--創(chuàng)建一個類,表示四邊形
local RectAngle = {length, width, area}
--聲明類名和類成員變量
function RectAngle: new (len,wid)
--聲明新建實例的New方法
local o = {
? ? ? --設(shè)定各個項的值
? ? ? length = len or 0,
? ? ? width = wid or 0,
? ? ? area =len*wid
???? ?}
setmetatable(o,{__index = self} )?--將自身的表映射到新new出來的表中 ? return o
end
然后來個比較簡單、優(yōu)化的例子,假設(shè)我們最后用到100個矩形:
local table_new = require "table.new"
local rect_length = table_new(100,0)
local rect_width = table_new(100,0)
local rect_area = table_new(100,0)
local rect_num = 0
function rect_new(length,width)
? ?rect_num += 1
? ?rect_length[rect_num] = length or 0
? ?rect_width[rect_num] = width or 0
? ?rect_area[rect_num] = length*width or 0
? ?return rect_num
end
function rect_get(id)
? ?return rect_length[id],rect_width[id],rect_area[id]
end
溜了溜了(