轉(zhuǎn)載?https://segmentfault.com/a/1190000004372649#articleHeader0
此為?Lua Programming Gems?一書的第二章:Lua Performance Tips亚茬,作者為 Roberto Ierusalimschy箍土。
我的翻譯以?網(wǎng)上別人的翻譯?為基礎(chǔ),做了比較大的修改唆香,讀起來更通順功茴。
關(guān)于性能優(yōu)化的兩條格言:
規(guī)則 1:不要優(yōu)化
規(guī)則 2:還是不要優(yōu)化(僅限專家)
不要在缺乏恰當(dāng)度量(measurements)時(shí)試圖去優(yōu)化軟件。編程老手和菜鳥之間的區(qū)別不是說老手更善于洞察程序的性能瓶頸,而是老手知道他們并不善于此戚篙。
做性能優(yōu)化離不開度量。優(yōu)化前度量溺职,可知何處需要優(yōu)化岔擂。優(yōu)化后度量,可知「優(yōu)化」是否確實(shí)改進(jìn)了代碼浪耘。
基本事實(shí)
運(yùn)行代碼之前乱灵,Lua 會(huì)把源代碼翻譯(預(yù)編譯)成一種內(nèi)部格式,這種格式由一連串虛擬機(jī)的指令構(gòu)成七冲,與真實(shí) CPU 的機(jī)器碼很相似痛倚。接下來,這一內(nèi)部格式交由 C 代碼來解釋澜躺,基本上就是一個(gè)?while?循環(huán)蝉稳,里面有一個(gè)很大的?switch,一種指令對(duì)應(yīng)一個(gè)?case掘鄙。
也許你已從他處得知耘戚,自 5.0 版起,Lua 使用了一個(gè)基于寄存器的虛擬機(jī)通铲。這些「寄存器」跟 CPU 中真實(shí)的寄存器并無關(guān)聯(lián)毕莱,因?yàn)檫@種關(guān)聯(lián)既無可移植性,也受限于可用的寄存器數(shù)量颅夺。Lua 使用一個(gè)棧(由一個(gè)數(shù)組加上一些索引實(shí)現(xiàn))來存放它的寄存器朋截。每個(gè)活動(dòng)的(active)函數(shù)都有一份活動(dòng)記錄(activation record),活動(dòng)記錄占用棧的一小塊吧黄,存放著這個(gè)函數(shù)對(duì)應(yīng)的寄存器部服。因此,每個(gè)函數(shù)都有其自己的寄存器拗慨。由于每條指令只有 8 個(gè) bit 用來指定寄存器廓八,每個(gè)函數(shù)便可以使用多至 250 個(gè)寄存器。
Lua 的寄存器如此之多赵抢,預(yù)編譯時(shí)便能將所有的局部變量存到寄存器中剧蹂。所以,在 Lua 中訪問局部變量是很快的烦却。舉個(gè)例子宠叼, 如果?a?和?b?是局部變量,語句?a = a + b?只生成一條指令:ADD 0 0 1(假設(shè)?a?和?b?分別在寄存器?0?和?1中)。對(duì)比之下冒冬,如果?a?和?b?是全局變量伸蚯,生成上述加法運(yùn)算的指令便會(huì)如下:
GETGLOBAL00; aGETGLOBAL11; bADD001SETGLOBAL00; a
所以,不難證明简烤,要想改進(jìn) Lua 程序的性能剂邮,最重要的一條原則就是:使用局部變量(use locals)!
除了一些明顯的地方外横侦,另有幾處也可使用局部變量挥萌,可以助你擠出更多的性能。比如枉侧,如果在很長(zhǎng)的循環(huán)里調(diào)用函數(shù)瑞眼,可以先將這個(gè)函數(shù)賦值給一個(gè)局部變量。這個(gè)代碼:
fori =1,1000000dolocalx = math.sin(i)end
比如下代碼慢 30%:
localsin = math.sinfori =1,1000000dolocalx = sin(i)end
訪問外層局部變量(也就是外一層函數(shù)的局部變量)并沒有訪問局部變量快棵逊,但是仍然比訪問全局變量快伤疙。考慮如下代碼:
functionfoo(x)fori =1,1000000dox = x + math.sin(i)endreturnxendprint(foo(10))
我們可以通過在?foo?函數(shù)外面定義一個(gè)?sin?來優(yōu)化它:
localsin = math.sinfunctionfoo(x)fori =1,1000000dox = x + sin(i)endreturnxendprint(foo(10))
第二段代碼比第一段快 30%辆影。
與其他語言的編譯器相比徒像,Lua 的編譯器算是比較高效的,盡管如此蛙讥,編譯仍是一項(xiàng)繁重的任務(wù)锯蛀。所以,應(yīng)盡量避免在程序中編譯代碼(比如次慢,使用?loadstring?函數(shù))旁涤。除非需要真正動(dòng)態(tài)地執(zhí)行代碼,比如代碼是由用戶輸入的迫像,其他情況則很少需要編譯動(dòng)態(tài)的代碼劈愚。
舉個(gè)例子,下面的代碼創(chuàng)建一個(gè)包含 10000 個(gè)函數(shù)的表闻妓,表中的函數(shù)分別返回常量?1?到?10000:
locallim =10000locala = {}fori =1, limdoa[i] = loadstring(string.format("return %d", i))endprint(a[10]())--> 10
這段代碼運(yùn)行了 1.4 秒菌羽。
使用閉包,可以避免動(dòng)態(tài)編譯由缆。下面的代碼創(chuàng)建同樣的 10000 個(gè)函數(shù)只用了 1/10 的時(shí)間(0.14秒):
functionfk(k)returnfunction()returnkendendlocallim =100000locala = {}fori =1, limdoa[i] = fk(i)endprint(a[10]())--> 10
關(guān)于表
通常注祖,使用表(table)時(shí)并不需要知道它的實(shí)現(xiàn)細(xì)節(jié)。事實(shí)上均唉,Lua 盡力避免把實(shí)現(xiàn)細(xì)節(jié)暴露給用戶是晨。然而這些細(xì)節(jié)還是在表操作的性能中暴露出來了。所以舔箭,為了高效地使用表罩缴,了解一些 Lua 實(shí)現(xiàn)表的方法,不無益處。
Lua 實(shí)現(xiàn)表的算法頗為巧妙靴庆。每個(gè)表包含兩部分:數(shù)組(array)部分和哈希(hash)部分,數(shù)組部分保存的項(xiàng)(entry)以整數(shù)為鍵(key)怒医,從 1 到某個(gè)特定的 n炉抒,(稍后會(huì)討論 n 是怎么計(jì)算的。)所有其他的項(xiàng)(包括整數(shù)鍵超出范圍的)則保存在哈希部分稚叹。
顧名思義焰薄,哈希部分使用哈希算法來保存和查找鍵值。它使用的是開放尋址(open address)的表扒袖,意味著所有的項(xiàng)都直接存在哈希數(shù)組里塞茅。鍵值的主索引由哈希函數(shù)給出;如果發(fā)生沖突(兩個(gè)鍵值哈希到相同的位置)季率,這些鍵值就串成一個(gè)鏈表野瘦,鏈表的每個(gè)元素占用數(shù)組的一項(xiàng)。
當(dāng) Lua 想在表中插入一個(gè)新的鍵值而哈希數(shù)組已滿時(shí)飒泻,Lua 會(huì)做一次重新哈希(rehash)鞭光。重新哈希的第一步是決定新的數(shù)組部分和哈希部分的大小。所以 Lua 遍歷所有的項(xiàng)泞遗,并加以計(jì)數(shù)和分類惰许,然后取一個(gè)使數(shù)組部分用量過半的最大的 2 的指數(shù)值,作為數(shù)組部分的大小史辙。而哈希部分的大小則是一個(gè)容得下剩余項(xiàng)(即那些不適合放在數(shù)組部分的項(xiàng))的最小的 2 的指數(shù)值汹买。
當(dāng) Lua 創(chuàng)建一個(gè)空表時(shí),兩部分的大小都是 0聊倔,因此也就沒有為它們分配數(shù)組空間晦毙。看看如下代碼運(yùn)行時(shí)會(huì)發(fā)生些什么:
locala = {}fori =1,3doa[i] =trueend
一開始創(chuàng)建一個(gè)空表耙蔑。循環(huán)的第一次迭代時(shí)结序,賦值語句?a[1] = true?觸發(fā)了一次重新哈希;Lua 將表中的數(shù)組部分大小設(shè)為 1纵潦,而哈希部分仍為空徐鹤。循環(huán)的第二次迭代時(shí),賦值語句?a[2] = true?又觸發(fā)了一次重新哈希邀层,現(xiàn)在返敬,表中的數(shù)組部分大小為 2。最后寥院,第三次迭代還是觸發(fā)了一次重新哈希劲赠,數(shù)組部分的大小增至 4。
像下面這樣的代碼:
a = {}a.x =1; a.y =2; a.z =3
做的事情類似,大小增長(zhǎng)的卻是表的哈希部分凛澎。
對(duì)于大型的表霹肝,這些初始的開銷將會(huì)被整個(gè)創(chuàng)建過程平攤:創(chuàng)建 3 個(gè)元素的表需要進(jìn)行 3 次重新哈希,而創(chuàng)建一百萬個(gè)元素的表只需要 20 次塑煎。但是當(dāng)你創(chuàng)建幾千個(gè)小表時(shí)沫换,總開銷就會(huì)很顯著。
老版的 Lua 在創(chuàng)建空表時(shí)會(huì)預(yù)分配一些空位(如果沒記錯(cuò)最铁,是 4)讯赏,來避免這種創(chuàng)建小表時(shí)的初始開銷。不過冷尉,這樣又有浪費(fèi)內(nèi)存之嫌漱挎。比如,以僅有兩個(gè)項(xiàng)的表來表示點(diǎn)雀哨,每個(gè)點(diǎn)使用的內(nèi)存就是真正所需內(nèi)存的兩倍磕谅,那么創(chuàng)建幾百萬個(gè)點(diǎn)將會(huì)使你付出高昂的代價(jià)。這就是現(xiàn)在 Lua 不為空表預(yù)分配空位的原因雾棺。
如果你用的是 C怜庸,可以通過 Lua 的 API 函數(shù)?lua_createtable?來避免這些重新哈希。這個(gè)函數(shù)除了司空見慣的參數(shù)?lua_State?外垢村,另接受兩個(gè)參數(shù):新表數(shù)組部分的初始大小和哈希部分的初始大小割疾。只要這兩個(gè)參數(shù)給得恰當(dāng),就能避免初始時(shí)的重新哈希嘉栓。不過需要注意的是宏榕,Lua 只在重新哈希時(shí)才有機(jī)會(huì)去收縮(shrink)表。所以侵佃,如果你指定的初始大小大于實(shí)際所需麻昼,空間的浪費(fèi) Lua 可能永遠(yuǎn)都不會(huì)為你糾正。
如果你用的是 Lua馋辈,可以通過構(gòu)造器(constructors)來避免那些初始的重新哈希抚芦。當(dāng)你寫下?{true, true, true}?時(shí),Lua 就會(huì)事先知道新表的數(shù)組部分需要 3 個(gè)空位迈螟,并創(chuàng)建一個(gè)相應(yīng)大小的表叉抡。與此類似,當(dāng)你寫下?{x = 1, y = 2, z = 3}時(shí)答毫,Lua 就創(chuàng)建一個(gè)哈希部分包含 4 個(gè)空位的表褥民。舉例來說,下面的循環(huán)運(yùn)行了 2.0 秒:
fori =1,1000000dolocala = {}? a[1] =1; a[2] =2; a[3] =3end
如果以正確的大小來創(chuàng)建這個(gè)表洗搂,運(yùn)行時(shí)間就降到了 0.7 秒:
fori =1,1000000dolocala = {true,true,true}? a[1] =1; a[2] =2; a[3] =3end
然而消返,當(dāng)你寫下形如?{[1] = true, [2] = true, [3] = true}?這樣的語句時(shí)载弄,Lua 并沒有聰明到能夠檢測(cè)出給定的表達(dá)式(指那些字面數(shù)字)是在描述數(shù)組下標(biāo),所以它創(chuàng)建了一個(gè)哈希部分有 4 個(gè)空位的表撵颊,既浪費(fèi)內(nèi)存也浪費(fèi) CPU 時(shí)間宇攻。
表的兩個(gè)部分的大小只在表重新哈希時(shí)計(jì)算,而重新哈希只在表已全滿而又需要插入新元素時(shí)才會(huì)發(fā)生倡勇。因此逞刷,當(dāng)你遍歷一個(gè)表并把個(gè)中元素逐一刪除時(shí)(即設(shè)它們?yōu)?nil),表并不會(huì)縮小译隘。你得往表里插些新的元素,然后表才會(huì)真正去調(diào)整大小洛心。通常這不是一個(gè)問題:當(dāng)你持續(xù)地刪除和插入元素時(shí)(很多程序的典型情況)固耘,表的大小將保持穩(wěn)定。不過词身,你不該期望通過從一個(gè)大表里刪除一些數(shù)據(jù)來回收內(nèi)存厅目,更好的做法是刪除這個(gè)表本身。
有一則強(qiáng)制重新哈希的奇技淫巧法严,即往表里插入足夠的?nil?元素损敷。示例如下:
a = {}lim =10000000fori =1, limdoa[i] = iend-- 創(chuàng)建一個(gè)巨大的表print(collectgarbage("count"))-->196626fori =1, limdoa[i] =nilend-- 刪除其所有的元素print(collectgarbage("count"))-->196626fori = lim +1,2*limdoa[i] =nilend-- 插入大量nil元素print(collectgarbage("count"))--> 17
除非特殊情況需要,我并不推薦這種手法深啤,因?yàn)檫@樣做很慢拗馒,而且要知道多少元素才算「足夠」,也沒有簡(jiǎn)單易行的方法溯街。
你可能會(huì)想诱桂,Lua 為什么不在我們插入?nil?時(shí)收縮表的大小呢?首先呈昔,是為了避免對(duì)插入元素的檢查挥等;一條檢查?nil?賦值的語句將會(huì)拖慢所有的賦值語句。其次堤尾,也是更重要的肝劲,是為了允許在遍歷表時(shí)對(duì)元素賦?nil?值」Γ考慮如下循環(huán):
fork, vinpairs(t)doifsome_property(v)thent[k] =nil-- 刪除這個(gè)元素endend
如果 Lua 在?nil?賦值后進(jìn)行重新哈希辞槐,那么這個(gè)遍歷就被破壞了。
如果你想刪除表中的所有元素粘室,正確的方法是使用一個(gè)簡(jiǎn)單的循環(huán):
forkinpairs(t)dot[k] =nilend
或者使用"聰明"一點(diǎn)的方法:
whiletruedolocalk =next(t)ifnotkthenbreakendt[k] =nilend
不過催蝗,這個(gè)循環(huán)在表很大時(shí)會(huì)很慢。調(diào)用函數(shù)?next?時(shí)育特,如果沒有傳入前一個(gè)鍵值丙号,返回的便是表的「第一個(gè)」元素(以某種隨機(jī)順序)先朦。(譯:「第一個(gè)」之所以加引號(hào),是指就表內(nèi)部的數(shù)組結(jié)構(gòu)而言的第一個(gè)元素犬缨,「以某種隨機(jī)順序」則是從表的角度或用戶使用表的角度來說喳魏。)為此,next?從頭遍歷表的數(shù)組空間(譯:包含數(shù)組和哈希兩部分)怀薛,查找一個(gè)非?nil元素刺彩。隨著循環(huán)逐一將這些第一個(gè)元素設(shè)為?nil,查找第一個(gè)非?nil?元素變得越來越久枝恋。結(jié)果是创倔,為了清除一個(gè)有 100000 個(gè)元素的表,這個(gè)“聰明”的循環(huán)用了 20 秒焚碌,而使用?pairs?遍歷表的循環(huán)只用了 0.04 秒畦攘。
關(guān)于字符串
和表一樣,了解 Lua 實(shí)現(xiàn)字符串的細(xì)節(jié)對(duì)高效地使用字符串也會(huì)有所幫助十电。
Lua 實(shí)現(xiàn)字符串的方式和大多數(shù)其他的腳本語言有兩點(diǎn)重要的區(qū)別知押。其一,Lua 的字符串都是內(nèi)化的(internalized)鹃骂;這意味著字符串在 Lua 中都只有一份拷貝台盯。每當(dāng)一個(gè)新字符串出現(xiàn)時(shí),Lua 會(huì)先檢查這個(gè)字符串是否已經(jīng)有一份拷貝畏线,如果有静盅,就重用這份拷貝。內(nèi)化(internalization)使字符串比較及表索引這樣的操作變得非城夼梗快温亲,但是字符串的創(chuàng)建會(huì)變慢。
其二杯矩,Lua 的字符串變量從來不會(huì)包含字符串本身栈虚,包含的只是字符串的引用。這種實(shí)現(xiàn)加快了某些字符串操作史隆。比如魂务,對(duì) Perl 來說,如果你寫下這樣的語句:$x = $y泌射,$y?包含一個(gè)字符串粘姜,這個(gè)賦值語句將復(fù)制?$y?緩沖區(qū)里的字符串內(nèi)容到?$x?的緩沖區(qū)中。如果字符串很長(zhǎng)熔酷,這一操作代價(jià)將非常高孤紧。而對(duì) Lua 來說,這樣的賦值語句只不過復(fù)制了一個(gè)指向?qū)嶋H字符串的指針拒秘。
這種使用引用的實(shí)現(xiàn)号显,使某種特定形式的字符串連接變慢了臭猜。在 Perl 里,$s = $s . "x"?和?$s .= "x"?這兩者是很不一樣的押蚤。前一個(gè)語句蔑歌,先得到一份?$s?的拷貝,然后往這份拷貝的末尾加上?"x"揽碘。后一個(gè)語句次屠,只是簡(jiǎn)單地把?"x"?追加到變量?$s?所持有的內(nèi)部緩沖區(qū)上。所以雳刺,第二種連接形式跟字符串大小是無關(guān)的(假設(shè)緩沖區(qū)有足夠的空間來存放連接的字符串)劫灶。如果在循環(huán)中執(zhí)行這兩條語句,那么它們的區(qū)別就是算法復(fù)雜度的線性階和平方階的區(qū)別了掖桦。比如本昏,以下循環(huán)讀一個(gè) 5MB 的文件,幾乎用了 5 分鐘:
$x ="";while(<>) {? $x = $x . $_;}
如果將?$x = $x . $_?替換成?$x .= $_滞详,則只要 0.1 秒凛俱!
Lua 并沒有提供這第二種較快的方法紊馏,因?yàn)?Lua 的變量并沒有與之關(guān)聯(lián)的緩沖區(qū)料饥。所以,我們必須使用一個(gè)顯式的緩沖區(qū):包含字符串片段的表就行朱监。以下循環(huán)還是讀 5MB 的文件岸啡,費(fèi)時(shí) 0.28 秒。沒 Perl 那么快赫编,不過也不賴巡蘸。
localt = {}forlineinio.lines()dot[#t +1] = lineends = table.concat(t,"\n")
減少,重用擂送,回收
當(dāng)處理 Lua 資源時(shí)悦荒,我們應(yīng)當(dāng)遵守跟利用地球資源一樣的?3R 原則。
減少(reduce)是最簡(jiǎn)單的一種途徑嘹吨。有幾種方法可以避免創(chuàng)建對(duì)象搬味。例如,如果你的程序使用了大量的表蟀拷,或許可以考慮改變它的數(shù)據(jù)表示碰纬。舉個(gè)簡(jiǎn)單的例子,假如你的程序需要處理折線(polyline)问芬。在 Lua 里悦析,折線最自然的表示是使用一個(gè)點(diǎn)的列表,像這樣:
polyline = {? { x =10.3, y =98.5},? { x =10.3, y =18.3},? { x =15.0, y =98.5},? ...}
這種表示雖然自然此衅,折線較大時(shí)卻不經(jīng)濟(jì)强戴,因?yàn)槊總€(gè)點(diǎn)都要用一個(gè)表亭螟。下面這種表示改用數(shù)組,內(nèi)存略為節(jié)首锰:
polyline = {? {10.3,98.5},? {10.3,18.3},? {15.0,98.5},? ...}
對(duì)于一條有一百萬個(gè)點(diǎn)的折線媒佣,這種改變使內(nèi)存用量從 95KB 降到 65KB。當(dāng)然陵刹,作為代價(jià)默伍,程序的可讀性有所損失:p[i].x?要比?p[i][4]?易懂得多。
還有一個(gè)更經(jīng)濟(jì)的方法衰琐,用兩個(gè)列表也糊,一個(gè)存?x?坐標(biāo)的值,一個(gè)存?y?坐標(biāo)的值:
polyline = {? x = {10.3,10.3,15.0, ...},? y = {98.5,18.3,98.5, ...}}
之前的?p[i].x?現(xiàn)在就是?p.x[i]羡宙。使用這種方式狸剃,一條有一百萬個(gè)點(diǎn)的折線只需 24KB 的內(nèi)存。
循環(huán)是尋找降低不必要資源創(chuàng)建的好地方狗热。例如钞馁,如果在循環(huán)中創(chuàng)建了一個(gè)常量的(constant)表,便可以把表移到循環(huán)之外匿刮,或者甚至可以移到外圍函數(shù)之外僧凰。比較如下兩段代碼:
functionfoo(...)fori =1, ndolocalt = {1,2,3,"hi"}-- 做一些不改變 t 的操作...endend
localt = {1,2,3,"hi"}-- 一次性地創(chuàng)建 tfunctionfoo(...)fori =1, ndo-- 做一些不改變 t 的操作...endend
同樣的技巧也可以用于閉包,只要移動(dòng)時(shí)不致越出閉包所需變量的作用域熟丸。例如训措,考慮以下函數(shù):
functionchangenumbers(limit, delta)forlinein io.lines()doline = string.gsub(line,"%d+",function(num)num =tonumber(num)ifnum >= limitthenreturntostring(num + delta)end-- else return nothing, keeping the original numberend)? ? io.write(line,"\n")endend
只要將內(nèi)部(inner)函數(shù)移到循環(huán)之外,就可避免為每一行都創(chuàng)建一個(gè)新的閉包:
functionchangenumbers(limit, delta)localfunctionaux(num)num =tonumber(num)ifnum >= limitthenreturntostring(num + delta)endendforlinein io.lines()doline = string.gsub(line,"%d+", aux)? ? io.write(line,"\n")endend
不過光羞,不能將函數(shù)?aux?移到函數(shù)?changenumbers?之外绩鸣,那樣的話,函數(shù)?aux?就不能訪問變量?limit?和?delta?了纱兑。
很多字符串的處理呀闻,都可以通過在現(xiàn)有字符串上使用下標(biāo),來避免創(chuàng)建不必要的新字符串潜慎。例如捡多,函數(shù)?string.find?返回的是給定模式出現(xiàn)的位置,而不是一個(gè)與之匹配的字符串勘纯。返回下標(biāo)局服,就避免了在成功匹配時(shí)創(chuàng)建一個(gè)新的(子)字符串。若有需要驳遵,可以再通過函數(shù)?string.sub?來獲取匹配的子字符串淫奔。
即使不能避免使用新的對(duì)象,也可以通過?重用(reuse)來避免創(chuàng)建新的對(duì)象堤结。對(duì)字符串來說唆迁,重用是沒有必要的鸭丛,因?yàn)?Lua 已經(jīng)替我們這樣做了:所有的字符串都是內(nèi)化的(internalized),因此只要可能就會(huì)重用唐责。對(duì)表來說鳞溉,重用就顯得卓有成效了。舉一個(gè)常見的例子鼠哥,讓我們回到在循環(huán)內(nèi)創(chuàng)建表的情況熟菲。不同的是,這次的表是可變的(not constant)朴恳。不過抄罕,往往只需簡(jiǎn)單的改變內(nèi)容,還是可以在所有的迭代中重用同一個(gè)表的于颖〈艋撸考慮以下代碼:
localt = {}fori =1970,2000dot[i] = os.time({year = i, month =6, day =14})end
以下代碼與之等價(jià),但是重用了表:
localt = {}localaux = {year =nil, month =6, day =14}fori =1970,2000doaux.year = i? t[i] = os.time(aux)end
實(shí)現(xiàn)重用的一種特別有效的方法是記憶化(memoizing)森渐∽鋈耄基本想法非常簡(jiǎn)單:對(duì)于一個(gè)給定的輸入,保存其計(jì)算結(jié)果同衣,當(dāng)遇到同樣的輸入時(shí)竟块,程序只需重用之前保存的結(jié)果。
來看看?LPeg(Lua 中一個(gè)新的模式匹配的包)彩郊,它使用記憶化的方式頗為有趣前弯。LPeg?把每個(gè)模式都編譯成一種內(nèi)部表示蚪缀,對(duì)負(fù)責(zé)匹配的分析器來說,這種表示就是一種「程序」恕出。這種編譯相對(duì)于匹配本身來說是比較費(fèi)時(shí)的询枚。因此為了重用,LPeg便記住編譯的結(jié)果浙巫,方式是用一個(gè)表金蜀,把描述模式的字符串和相應(yīng)的內(nèi)部表示關(guān)聯(lián)起來。
記憶化方法的一個(gè)比較普遍的問題是的畴,保存之前結(jié)果而在空間上的花費(fèi)可能會(huì)甚于重用這些結(jié)果的好處渊抄。為了解決這個(gè)問題,我們可以使用弱表(weak table)丧裁,這樣护桦,不用的結(jié)果最后就會(huì)從表中刪除。
借助于高階函數(shù)(higher-order functions)煎娇,我們可以定義一個(gè)通用的記憶化函數(shù):
functionmemoize(f)localmem = {}-- memoizing tablesetmetatable(mem, {__mode="kv"})-- make it weakreturnfunction(x)-- new version of 'f', with memoizinglocalr = mem[x]ifr ==nilthen-- no previous result?r = f(x)-- calls original functionmem[x] = r-- store result for reuseendreturnrendend
對(duì)于一個(gè)給定的函數(shù)?f二庵,memoize(f)?返回一個(gè)新的函數(shù)贪染,這個(gè)函數(shù)會(huì)返回跟?f?一樣的結(jié)果,但是會(huì)把結(jié)果記錄下來催享。例如杭隙,我們可以重新定義?loadstring?函數(shù)的一個(gè)記憶化版本:
loadstring = memoize(loadstring)
新函數(shù)的使用方式和老函數(shù)一樣,但是如果我們加載的字符串中有很多重復(fù)的字符串因妙,便會(huì)獲得很大的性能提升痰憎。
如果你的程序創(chuàng)建和釋放過多的協(xié)程(coroutines),也許可以通過?回收(recycle)來提高它的性能攀涵。目前協(xié)程的 API 并沒有直接提供重用協(xié)程的方法信殊,但是我們可以設(shè)法克服這一限制≈考慮以下協(xié)程:
co = coroutine.create(function(f)whilefdof = coroutine.yield(f())endend
這個(gè)協(xié)程接受一個(gè)作業(yè)(job)(一個(gè)待執(zhí)行的函數(shù))涡拘,執(zhí)行這個(gè)作業(yè),結(jié)束后等待下一個(gè)作業(yè)据德。
Lua 中的大多數(shù)回收都是由垃圾收集器自動(dòng)完成的鳄乏。Lua 使用一個(gè)增量(incremental)的垃圾收集器,逐步(in small steps)回收(增量地)棘利,跟程序一起交錯(cuò)執(zhí)行橱野。每一步回收多少,跟內(nèi)存分配成正比:Lua 分配了多少內(nèi)存善玫,垃圾收集器就做多少相應(yīng)比例的工作水援。程序消耗內(nèi)存越快,收集器嘗試回收內(nèi)存也就越快茅郎。
如果我們?cè)诔绦蛑凶袷販p少和重用的原則蜗元,收集器通常沒有太多的事情可做。但是有時(shí)候我們不能避免創(chuàng)建大量的垃圾系冗,這時(shí)收集器就可能變得任務(wù)繁重了奕扣。Lua 的垃圾收集器是為一般的程序而設(shè)的,對(duì)大多數(shù)應(yīng)用來說掌敬,它的表現(xiàn)都是相當(dāng)不錯(cuò)的惯豆。但是有時(shí)候,某些特殊的應(yīng)用場(chǎng)景奔害,適當(dāng)?shù)卣{(diào)整收集器還是可以提高性能的楷兽。
要控制垃圾收集器,可以調(diào)用 Lua 的函數(shù)?collectgarbage华临,或者 C 函數(shù)?lua_gc芯杀。盡管接口不同,這兩個(gè)函數(shù)的功能基本一致。接下來的討論我會(huì)使用 Lua 函數(shù)瘪匿,雖然這種操作往往更適合在 C 里面做跛梗。
函數(shù)?collectgarbage?提供了這樣幾種功能:它可以停止和重啟收集器,強(qiáng)制進(jìn)行一次完整的收集棋弥,強(qiáng)制執(zhí)行一步收集(collection step)核偿,得到當(dāng)前內(nèi)存使用總量,更改兩個(gè)影響收集效率(pace)的參數(shù)顽染。所有這些操作在缺乏內(nèi)存的程序里都有其用武之地漾岳。
對(duì)于某些批處理程序(batch programs),可以考慮「永遠(yuǎn)」地停止收集器粉寞。這些批處理程序通常都是先創(chuàng)建一些數(shù)據(jù)結(jié)構(gòu)尼荆,并根據(jù)那些結(jié)構(gòu)體產(chǎn)生一些輸出,然后就退出(比如編譯器)唧垦。對(duì)于那些程序捅儒,試圖去收集垃圾也許就比較浪費(fèi)時(shí)間了,因?yàn)闆]什么垃圾可回收的振亮,并且程序一旦退出巧还,所有的內(nèi)存就會(huì)得到釋放。
對(duì)于非批處理的程序坊秸,永遠(yuǎn)停止收集器并不可取麸祷。不過,在一些關(guān)鍵的時(shí)間點(diǎn)褒搔,停止收集器對(duì)程序可能卻是有益的阶牍。如有必要,還可以由程序來完全控制垃圾收集器星瘾,讓它總是處于停止?fàn)顟B(tài)走孽,只在程序顯式地要求執(zhí)行一個(gè)步驟或者執(zhí)行一個(gè)完整的回收時(shí),收集器才開始工作死相。例如融求,有些事件驅(qū)動(dòng)的平臺(tái)會(huì)提供一個(gè)?idle?函數(shù)咬像,這個(gè)函數(shù)會(huì)在沒有事件可以處理時(shí)被調(diào)用算撮。這是執(zhí)行垃圾收集的最佳時(shí)刻。(Lua5.1 中县昂,在收集器停止時(shí)去強(qiáng)制執(zhí)行一些收集操作肮柜,都會(huì)使收集器自動(dòng)重啟。所以為了保持它停止的狀態(tài)倒彰,必須在強(qiáng)制執(zhí)行一些收集操作之后馬上調(diào)用?collectgarbage?("stop")审洞。)
最后一個(gè)方法,可以試著改變收集器的參數(shù)。收集器由兩個(gè)參數(shù)控制其收集的步長(zhǎng)(pace)芒澜。第一個(gè)是?pause仰剿,控制收集器在一輪回收結(jié)束后隔多久才開始下一輪的回收。第二個(gè)參數(shù)是?stepmul痴晦,控制收集器每一步要做多少工作南吮。粗略地講,pause?越小誊酌,stepmul?越大部凑,收集器工作就越快。
這些參數(shù)對(duì)一個(gè)程序的總體性能的影響是很難預(yù)測(cè)的碧浊。收集器越快涂邀,其每秒耗費(fèi)的 CPU 周期顯然也就越多;但是另一方面箱锐,或許這樣能減少程序的內(nèi)存使用總量比勉,從而減少換頁(paging)。只有通過仔細(xì)的實(shí)驗(yàn)驹止,才能為這些參數(shù)找到最佳的值敷搪。