和在所有其他編程語言中一樣,在Lua中,我們依然應當遵循下述兩條有關程序優(yōu)化的箴言:
原則1:不要做優(yōu)化。
原則2:暫時不要做優(yōu)化(對專家而言)。
這兩條原則對于Lua編程來說尤其有意義,Lua正是因其性能而在腳本語言中鶴立雞群。
當然,我們都知道性能是編程中要考量的一個重要因素,指數(shù)級時間復雜度的算法會被認為是棘手的問題,絕非偶然。如果計算結果來得太遲,它就是無用的結果。因此,每一個優(yōu)秀的程序員都應該時刻平衡在優(yōu)化代碼時所花費的資源和執(zhí)行代碼時所節(jié)省的資源。
優(yōu)秀的程序員對于代碼優(yōu)化要提出的第一個問題是:“這個程序需要被優(yōu)化嗎?”如果(僅當此時)答案是肯定的,第二個問題則是:“在哪里優(yōu)化?”
要回答這樣兩個問題,我們需要制定一些標準。在進行有效的性能評定之前,不應該做任何優(yōu)化工作。有經驗的程序員和初學者之前的區(qū)別并非在于前者善于指出一個程序的主要性能開銷所在,而是前者知道自己不善于做這件事情。
幾年前,Noemi Rodriguez和我開發(fā)了一個用于Lua的CORBA ORB[2]原型,之后演變?yōu)镺iL。作為第一個原型,我們的實現(xiàn)的目標是簡潔。為防止對額外的C函數(shù)庫的依賴,這個原型在序列化整數(shù)時使用少量四則運算來分離各個字節(jié)(轉換為以256為底),且不支持浮點值。由于CORBA視字符串為字符序列,我們的ORB最初也將Lua字符串轉換為一個字符序列(也就是一個Lua表),并且將其和其他序列等同視之。
當我們完成這個原型之后,我們把它的性能和一個使用C++實現(xiàn)的專業(yè)ORB進行對比。由于我們的ORB是使用Lua實現(xiàn)的,預期上我們可以容忍它的速度要慢一些,但是對比結果顯示它慢得太多了,讓我們非常失望。一開始,我們把責任歸結于Lua本身;后來我們懷疑問題出在那些需要序列化整數(shù)的操作上。我們使用了一個非常簡單的性能分析器(Profiler),與在《Lua程序設計》[3]第23章里描述的那個沒什么太大差別。出乎我們意料的是,整數(shù)序列化并沒有明顯拖慢程序的速度,因為并沒有太多整數(shù)需要序列化;反而是序列化字符串需要對低性能負很大責任。實際上,每一條CORBA消息都包含若干個字符串,即使我們沒有顯式地操作字符串亦是如此。而且序列化每一條字符串都是一個性能開銷巨大的工作,因為它需要創(chuàng)建一個新表,并使用單獨的字符填充;然后序列化整個序列,其中需要依次序列化每個字符。一旦我們將字符串序列化作為一種特殊情況(而不是通過通用的序列化流程)重新實現(xiàn),整個程序的性能就得到了顯著的提升。我們只是添加了幾行代碼,程序的性能已經和C++實現(xiàn)的那個版本有得一拼了[4]。
因此,我們總是應該在優(yōu)化性能之前進行性能測試。通過測試,才能了解到要優(yōu)化什么;在優(yōu)化后再次測試,來確認我們的優(yōu)化工作確實帶來了性能的提升。
一旦你決定必須優(yōu)化你的Lua代碼,本文將可能有所幫助。本文描述了一些優(yōu)化方式,主要是展示在Lua中怎么做會更慢,怎么做又會更快。在這里,我將不會討論一些通用的優(yōu)化技巧,例如優(yōu)化算法等等——當然,你應該掌握和使用這些技巧,有很多其他地方可以了解這方面的內容。本文主要討論一些專門針對Lua的優(yōu)化技巧,與此同時,我還會持續(xù)地測試小程序的時間和空間性能。如果沒有特別注明的話,所有的測試都在一臺Pentium IV 2.9GHz、1GB內存、運行Ubuntu 7.10、Lua 5.1.1的機器上進行。我經常會給出實際的測量結果(例如7秒),但是這只在和其他測量數(shù)據(jù)進行對比時有意義。而當我說一個程序比另一個快X%時,意味著前者比后者少消耗X%的時間(也就是說,比另一個程序快100%的程序的運行不需要時間);當我說一個程序比另一個慢X%時,則是說后者比前者快X%(意即,比另一個程序慢50%的程序消耗的時間是前者的兩倍)。
在運行任何代碼之前,Lua都會把源代碼翻譯(預編譯)成一種內部的格式。這種格式是一個虛擬機指令序列,與真實的CPU所執(zhí)行的機器碼類似。之后,這個內部格式將會被由一個包含巨大的switch結構的while循環(huán)組成的C代碼解釋執(zhí)行,switch中的每個case對應一條指令。
可能你已經在別處了解到,從5.0版開始,Lua使用一種基于寄存器的虛擬機。這里所說的虛擬機“寄存器”與真正的CPU寄存器并不相同,因為后者難于移植,而且數(shù)量非常有限。Lua使用一個棧(通過一個數(shù)組和若干索引來實現(xiàn))來提供寄存器。每個活動的函數(shù)都有一個激活記錄,也就是棧上的一個可供該函數(shù)存儲寄存器的片段。因此,每個函數(shù)都有自己的寄存器[1]。一個函數(shù)可以使用最多250個寄存器,因為每個指令只有8位用于引用一個寄存器。
由于寄存器數(shù)目眾多,因此Lua預編譯器可以把所有的局部變量都保存在寄存器里。這樣帶來的好處是,訪問局部變量會非???。例如,如果a和b是局部變量,語句
a = a + b
將只會生成一個指令:
ADD 0 0 1
(假設a和b在寄存器里分別對應0和1)。作為對比,如果a和b都是全局變量,那么這段代碼將會變成:
GETGLOBAL 0 0 ; aGETGLOBAL 1 1 ; bADD 0 0 1SETGLOBAL 0 0 ; a
因此,可以很簡單地得出在Lua編程時最重要的性能優(yōu)化方式:使用局部變量!
如果你想壓榨程序的性能,有很多地方都可以使用這個方法。例如,如果你要在一個很長的循環(huán)里調用一個函數(shù),可以預先將這個函數(shù)賦值給一個局部變量。比如說如下代碼:
for i = 1, 1000000 do local x = math.sin(i)end
比下面這段要慢30%:
local sin = math.sinfor i = 1, 1000000 do local x = sin(i)end
訪問外部局部變量(或者說,函數(shù)的上值)沒有直接訪問局部變量那么快,但依然比訪問全局變量要快一些。例如下面的代碼片段:
可以優(yōu)化為在foo外聲明一次sin:
local sin = math.sinfunction foo (x) for i = 1, 1000000 do x = x + sin(i) end return xendprint(foo(10))
第二段代碼比前者要快30%。
盡管比起其他語言的編譯器來說,Lua的編譯器非常高效,但是編譯依然是重體力活。因此,應該盡可能避免運行時的編譯(例如使用loadstring函數(shù)),除非你真的需要有如此動態(tài)要求的代碼,例如由用戶輸入的代碼。只有很少的情況下才需要動態(tài)編譯代碼。
例如,下面的代碼創(chuàng)建一個包含返回常數(shù)值1到100000的若干個函數(shù)的表:
local lim = 10000local a = {}for i = 1, lim do a[i] = loadstring(string.format("return %d", i))endprint(a[10]()) --> 10
執(zhí)行這段代碼需要1.4秒。
一般情況下,你不需要知道Lua實現(xiàn)表的細節(jié),就可以使用它。實際上,Lua花了很多功夫來隱藏內部的實現(xiàn)細節(jié)。但是,實現(xiàn)細節(jié)揭示了表操作的性能開銷情況。因此,要優(yōu)化使用表的程序(這里特指Lua程序),了解一些表的實現(xiàn)細節(jié)是很有好處的。
Lua的表的實現(xiàn)使用了一些很聰明的算法。每個Lua表的內部包含兩個部分:數(shù)組部分和哈希部分。數(shù)組部分以從1到一個特定的n之間的整數(shù)作為鍵來保存元素(我們稍后即將討論這個n是如何計算出來的)。所有其他元素(包括在上述范圍之外的整數(shù)鍵)都被存放在哈希部分里。
正如其名,哈希部分使用哈希算法來保存和查找鍵。它使用被稱為開放地址表的實現(xiàn)方式,意思是說所有的元素都保存在哈希數(shù)組中。用一個哈希函數(shù)來獲取一個鍵對應的索引;如果存在沖突的話(意即,如果兩個鍵產生了同一個哈希值),這些鍵將會被放入一個鏈表,其中每個元素對應一個數(shù)組項。當Lua需要向表中添加一個新的鍵,但哈希數(shù)組已滿時,Lua將會重新哈希。重新哈希的第一步是決定新的數(shù)組部分和哈希部分的大小。因此,Lua遍歷所有的元素,計數(shù)并對其進行歸類,然后為數(shù)組部分選擇一個大小,這個大小相當于能使數(shù)組部分超過一半的空間都被填滿的2的最大的冪;然后為哈希部分選擇一個大小,相當于正好能容納哈希部分所有元素的2的最小的冪。
當Lua創(chuàng)建空表時,兩個部分的大小都是0。因此,沒有為其分配數(shù)組。讓我們看一看當執(zhí)行下面的代碼時會發(fā)生什么:
local a = {}for i = 1, 3 do a[i] = trueend
這段代碼始于創(chuàng)建一個空表。在循環(huán)的第一次迭代中,賦值語句
a[1] = true
觸發(fā)了一次重新哈希;Lua將數(shù)組部分的大小設為1,哈希部分依然為空;第二次迭代時
a[2] = true
觸發(fā)了另一次重新哈希,將數(shù)組部分擴大為2.最終,第三次迭代又觸發(fā)了一次重新哈希,將數(shù)組部分的大小擴大為4。
類似下面的代碼
a = {}a.x = 1; a.y = 2; a.z = 3
做的事情類似,只不過增加的是哈希部分的大小。
對于大的表來說,初期的幾次重新哈希的開銷被分攤到整個表的創(chuàng)建過程中,一個包含三個元素的表需要三次重新哈希,而一個有一百萬個元素的表也只需要二十次。但是當創(chuàng)建幾千個小表的時候,重新哈希帶來的性能影響就會非常顯著。
舊版的Lua在創(chuàng)建空表時會預選分配大?。?,如果我沒有記錯的話),以防止在初始化小表時產生的這些開銷。但是這樣的實現(xiàn)方式會浪費內存。例如,如果你要創(chuàng)建數(shù)百萬個點(表現(xiàn)為包含兩個元素的表),每個都使用了兩倍于實際所需的內存,就會付出高昂的代價。這也是為什么Lua不再為新表預分配數(shù)組。
如果你使用C編程,可以通過Lua的API函數(shù)lua_createtable來避免重新哈希;除lua_State之外,它還接受兩個參數(shù):數(shù)組部分的初始大小和哈希部分的初始大小[1]。只要指定適當?shù)闹?,就可以避免初始化時的重新哈希。需要警惕的是,Lua只會在重新哈希時收縮表的大小,因此如果在初始化時指定了過大的值,Lua可能永遠不會糾正你浪費的內存空間。
當使用Lua編程時,你可能可以使用構造式來避免初始化時的重新哈希。當你寫下
{true, true, true}
時,Lua知道這個表的數(shù)組部分將會有三個元素,因此會創(chuàng)建相應大小的數(shù)組。類似的,如果你寫下
{x = 1, y = 2, z = 3}
Lua也會為哈希部分創(chuàng)建一個大小為4的數(shù)組。例如,執(zhí)行下面的代碼需要2.0秒:
for i = 1, 1000000 do local a = {} a[1] = 1; a[2] = 2; a[3] = 3end
如果在創(chuàng)建表時給定正確的大小,執(zhí)行時間可以縮減到0.7秒:
for i = 1, 1000000 do local a = {true, true, true} a[1] = 1; a[2] = 2; a[3] = 3end
但是,如果你寫類似于
{[1] = true, [2] = true, [3] = true}
的代碼,Lua還不夠聰明,無法識別表達式(在本例中是數(shù)值字面量)指定的數(shù)組索引,因此它會為哈希部分創(chuàng)建一個大小為4的數(shù)組,浪費內存和CPU時間。
兩個部分的大小只會在Lua重新哈希時重新計算,重新哈希則只會發(fā)生在表完全填滿后,Lua需要插入新的元素之時。因此,如果你遍歷一個表并清除其所有項(也就是全部設為nil),表的大小不會縮小。但是此時,如果你需要插入新的元素,表的大小將會被調整。多數(shù)情況下這都不會成為問題,但是,不要指望能通過清除表項來回收內存:最好是直接把表自身清除掉。
一個可以強制重新哈希的猥瑣方法是向表中插入足夠多的nil。例如:
a = {}lim = 10000000for i = 1, lim do a[i] = i end -- 創(chuàng)建一個巨表print(collectgarbage("count")) --> 196626for i = 1, lim do a[i] = nil end -- 清除所有元素print(collectgarbage("count")) --> 196626for i = lim + 1, 2 * lim do a[i] = nil end -- 創(chuàng)建一堆nil元素print(collectgarbage("count")) --> 17
除非是在特殊情況下,我不推薦使用這個伎倆:它很慢,并且沒有簡單的方法能知道要插入多少nil才夠。
你可能會好奇Lua為什么不會在清除表項時收縮表。首先是為了避免測試寫入表中的內容。如果在賦值時檢查值是否為nil,將會拖慢所有的賦值操作。第二,也是最重要的,允許在遍歷表時將表項賦值為nil。例如下面的循環(huán):
for k, v in pairs(t) do if some_property(v) then t[k] = nil – 清除元素 endend
如果Lua在每次nil賦值后重新哈希這張表,循環(huán)就會被破壞。
如果你想要清除一個表中的所有元素,只需要簡單地遍歷它:
for k in pairs(t) do t[k] = nilend
一個“聰明”的替代解決方案:
while true do local k = next(t) if not k then break end t[k] = nilend
但是,對于大表來說,這個循環(huán)將會非常慢。調用函數(shù)next時,如果沒有給定前一個鍵,將會返回表的第一個元素(以某種隨機的順序)。在此例中,next將會遍歷這個表,從開始尋找一個非nil元素。由于循環(huán)總是將找到的第一個元素置為nil,因此next函數(shù)將會花費越來越長的時間來尋找第一個非nil元素。這樣的結果是,這個“聰明”的循環(huán)需要20秒來清除一個有100,000個元素的表,而使用pairs實現(xiàn)的循環(huán)則只需要0.04秒。
通過使用閉包,我們可以避免使用動態(tài)編譯。下面的代碼只需要十分之一的時間完成相同的工作:
function fk (k) return function () return k endendlocal lim = 100000local a = {}for i = 1, lim do a[i] = fk(i) endprint(a[10]()) --> 10
與表類似,了解Lua如何實現(xiàn)字符串可以讓你更高效地使用它。
Lua實現(xiàn)字符串的方式與多數(shù)其他腳本語言所采用的兩種主要方式都不相同。首先,Lua中的所有字符串都是內部化[1]的,這意味著Lua維護著任何字符串的一個單一拷貝。當一個新字符串出現(xiàn)時,Lua檢查是否有現(xiàn)成的拷貝,如果有的話,重用之。內部化使得諸如字符串對比和索引表之類的操作非??焖?,但是會降低創(chuàng)建字符串的速度。
第二,Lua中的變量從不存儲字符串,只是引用它們。這種實現(xiàn)方式可以加快很多字符串操作,例如在Perl中,當你寫類似于$x=$y的代碼、$y是一個字符串時,賦值操作會將字符串的內容從$y的緩沖區(qū)復制到$x的緩沖區(qū)。如果這個字符串很長,這個操作的開銷就很大。而在Lua中,這個賦值僅僅是一次指針的復制。
然而,這種引用實現(xiàn)會降低特定方式的字符串連接的速度。在Perl中,操作$s = $s . "x"和$s .= "x"區(qū)別非常大,對于前者,你獲得了$s的一個拷貝,并且追加"x"到它的尾部;而對于后者,"x"只是簡單地被追加到$s所維護的內部緩沖區(qū)的尾部。因此,后者無關于字符串的長度(假設緩沖區(qū)足夠放下追加的文本)。如果把這兩句代碼放進循環(huán)里,它們的區(qū)別就是線性和二次算法的區(qū)別。例如,下述循環(huán)需要大約五分鐘來讀取一個5MB的文件:
$x = "";while (<>){ $x = $x . $_;}
如果我們把
$x = $x . $_
改為
$x .= $_
耗時將會降低為0.1秒!
Lua沒有提供第二種,也就是更快速的方式,因為它的變量沒有內部緩沖區(qū)。因此,我們需要一個顯式的緩沖區(qū):一個包含字符串片段的表來完成這項工作。下面的循環(huán)讀取相同的5MB的文件,需要0.28秒,雖然沒有Perl那么快,也還算不錯:
local t = {}for line in io.lines() do t[#t + 1] = lineends = table.concat(t, "\n")
當處理Lua資源時,我們也應該遵循提倡用于地球資源的3R原則——Reduce, Reuse and Recycle,即削減、重用和回收。
削減是最簡單的方式。有很多方法可以避免使用新的對象,例如,如果你的程序使用了太多的表,可以考慮改變數(shù)據(jù)的表述形式。一個最簡單的例子,假設你的程序需要操作折線,最自然的表述形式是:
盡管很自然,這種表述形式對于大規(guī)模的折線來說卻不夠經濟,因為它的每個點都需要用一個表來描述。第一種替代方式是使用數(shù)組來記錄,可以省點內存:
對于一個有一百萬個點的折線來說,這個修改可以把內存占用從95KB降低到65KB。當然,你需要在可讀性上付出代價:p[i].x比p[i][1]更易懂。
另一個更經濟的做法是使用一個數(shù)組存儲所有x坐標,另一個存儲所有y坐標:
polyline ={ x = { 10.3, 10.3, 15.0, ...}, y = { 98.5, 18.3, 98.5, ...}}原有的
p[i].x現(xiàn)在變成了
p.x[i]使用這種表述形式,一百萬個點的折線的內存占用降低到了24KB。
循環(huán)是尋找降低垃圾回收次數(shù)的機會的好地方。例如,如果在循環(huán)里創(chuàng)建一個不會改變的表,你可以把它挪到循環(huán)外面,甚至移到函數(shù)外作為上值。試對比:
和
local t = {1, 2, 3, "hi"} -- 創(chuàng)建t,一勞永逸function foo (...) for i = 1, n do --做一些不會改變t表的事情 --... endend相同的技巧亦可用于閉包,只要你不把它們移到需要它們的作用域之外。例如下面的函數(shù):
function changenumbers (limit, delta) for line in io.lines() do line = string.gsub(line, "%d+", function (num) num = tonumber(num) if num >= limit then return tostring(num + delta) end -- 否則不返回任何值,保持原有數(shù)值 end) io.write(line, "\n") endend我們可以通過將內部的函數(shù)移到循環(huán)外面來避免為每次迭代創(chuàng)建新的閉包:
function changenumbers (limit, delta) local function aux (num) num = tonumber(num) if num >= limit then return tostring(num + delta) end end for line in io.lines() do line = string.gsub(line, "%d+", aux) io.write(line, "\n") endend但是,我們不能把aux移到changenumbers函數(shù)之外,因為aux需要訪問limit和delta。
對于多種字符串處理,我們可以通過使用現(xiàn)有字符串的索引來減少對創(chuàng)建新字符串的需要。例如,string.find函數(shù)返回它找到指定模式的位置索引,而不是匹配到的字符串。通過返回索引,它避免了在成功匹配時創(chuàng)建新的字符串。當有必要時,程序員可以通過調用string.sub來獲取匹配的子串[1]。
當我們無法避免使用新的對象時,我們依然可以通過重用來避免創(chuàng)建新的對象。對于字符串來說,重用沒什么必要,因為Lua已經為我們做了這樣的工作:它總是將所有用到的字符串內部化,并在所有可能的時候重用。然而對于表來說,重用可能就非常有效。舉一個普遍的例子,讓我們回到在循環(huán)里創(chuàng)建表的情況。這一次,表里的內容不再是不變的。通常我們可以在所有迭代中重用這個表,只需要簡單地改變它的內容??紤]如下的代碼段:
local t = {}for i = 1970, 2000 do t[i] = os.time({year = i, month = 6, day = 14})end下面的代碼是等同的,但是重用了這張表:
local t = {}local aux = {year = nil, month = 6, day = 14}for i = 1970, 2000 do aux.year = i t[i] = os.time(aux)end實現(xiàn)重用的一個尤其有效的方式是緩存化[2]?;舅枷敕浅:唵?,將指定輸入對應的計算結果存儲下來,當下一次再次接受相同的輸入時,程序只需簡單地重用上次的計算結果。
LPeg,Lua的一個新的模式匹配庫,就使用了一個有趣的緩存化處理。LPeg將每個模式字符串編譯為一個內部的用于匹配字符串的小程序,比起匹配本身而言,這個編譯過程開銷很大,因此LPeg將編譯結果緩存化以便重用。只需一個簡單的表,以模式字符串為鍵、編譯后的小程序為值進行記錄。
使用緩存化時常見的一個問題是,存儲計算結果所帶來的內存開銷大過重用帶來的性能提升。為了解決這個問題,我們可以在Lua里使用一個弱表來記錄計算結果,因此沒有使用到的結果最終將會被回收。
在Lua中,利用高階函數(shù),我們可以定義一個通用的緩存化函數(shù):
function memoize (f) local mem = {} -- 緩存化表 setmetatable(mem, {__mode = "kv"}) -- 設為弱表 return function (x) -- ‘f’緩存化后的新版本 local r = mem[x] if r == nil then --沒有之前記錄的結果? r = f(x) --調用原函數(shù) mem[x] = r --儲存結果以備重用 end return r endend對于任何函數(shù)f,memoize(f)返回與f相同的返回值,但是會將之緩存化。例如,我們可以重新定義loadstring為一個緩存化的版本:
loadstring = memoize(loadstring)新函數(shù)的使用方式與老的完全相同,但是如果在加載時有很多重復的字符串,性能會得到大幅提升。
如果你的程序創(chuàng)建和刪除太多的協(xié)程,循環(huán)利用將可能提高它的性能?,F(xiàn)有的協(xié)程API沒有直接提供重用協(xié)程的支持,但是我們可以設法繞過這一限制。對于如下協(xié)程:
co = coroutine.create(function (f) while f do f = coroutine.yield(f()) endend)這個協(xié)程接受一項工作(運行一個函數(shù)),執(zhí)行之,并且在完成時等待下一項工作。
Lua中的多數(shù)回收都是通過垃圾回收器自動完成的。Lua使用漸進式垃圾回收器,意味著垃圾回收工作會被分成很多小步,(漸進地)在程序的允許過程中執(zhí)行。漸進的節(jié)奏與內存分配的速度成比例,每當分配一定量的內存,就會按比例地回收相應的內存;程序消耗內存越快,垃圾回收器嘗試回收內存也就越快。
如果我們在編寫程序時遵循削減和重用的原則,通常垃圾回收器不會有太多的事情要做。但是有時我們無法避免制造大量的垃圾,垃圾回收器的工作也會變得非常繁重。Lua中的垃圾回收器被調節(jié)為適合平均水平的程序,因此它在多數(shù)程序中工作良好。但是,在特定的時候我們可以通過調整垃圾回收器來獲取更好的性能。通過在Lua中調用函數(shù)collectgarbage,或者在C中調用lua_gc,來控制垃圾回收器。它們的功能相同,只不過有不同的接口。在本例中我將使用Lua接口,但是這種操作通常在C中進行更好。
collectgarbage函數(shù)提供若干種功能:它可以停止或者啟動垃圾回收器、強制進行一次完整的垃圾回收、獲取Lua占用的總內存,或者修改影響垃圾回收器工作節(jié)奏的兩個參數(shù)。它們在調整高內存消耗的程序時各有用途。
“永遠”停止垃圾回收器可能對于某些批處理程序很有用。這些程序創(chuàng)建若干數(shù)據(jù)結構,根據(jù)它們生產出一些輸出值,然后退出(例如編譯器)。對于這樣的程序,試圖回收垃圾將會是浪費時間,因為垃圾量很少,而且內存會在程序執(zhí)行完畢后完整釋放。
對于非批處理程序,停止垃圾回收器則不是個好主意。但是,這些程序可以在某些對時間極度敏感的時期暫停垃圾回收器,以提高時間性能。如果有需要的話,這些程序可以獲取垃圾回收器的完全控制,使其始終處于停止狀態(tài),僅在特定的時候顯式地進行一次強制的步進或者完整的垃圾回收。例如,很多事件驅動的平臺都提供一個選項,可以設置空閑函數(shù),在沒有消息需要處理時調用。這正是調用垃圾回收的絕好時機(在Lua 5.1中,每當你在垃圾回收器停止的狀態(tài)下進行強制回收,它都會恢復運轉,因此,如果要保持垃圾回收器處于停止狀態(tài),必須在強制回收后立刻調用collectgarbage("stop"))。
最后,你可能希望實施調整回收器的參數(shù)。垃圾回收器有兩個參數(shù)用于控制它的節(jié)奏:第一個,稱為暫停時間,控制回收器在完成一次回收之后和開始下次回收之前要等待多久;第二個參數(shù),稱為步進系數(shù),控制回收器每個步進回收多少內容。粗略地來說,暫停時間越小、步進系數(shù)越大,垃圾回收越快。這些參數(shù)對于程序的總體性能的影響難以預測,更快的垃圾回收器顯然會浪費更多的CPU周期,但是它會降低程序的內存消耗總量,并可能因此減少分頁。只有謹慎地測試才能給你最佳的參數(shù)值。
正如我們在前言里所說,優(yōu)化是一個技巧性很強的工作,從程序是否需要優(yōu)化開始,有若干個方面的內容需要考量。如果程序真的有性能問題,那么我們應該將精力集中于優(yōu)化哪里和如何優(yōu)化。
我們在這里討論的技巧既不是唯一的,也不是最重要的方面。我們在這里專注于討論專門針對Lua的優(yōu)化方式,因為有很多其他的方式可以了解通用的程序優(yōu)化技巧。
在本文結束之前,我還想介紹兩種從更大的尺度上優(yōu)化Lua程序性能的方式,但是它們都牽涉到Lua代碼之外的修改。第一個是使用LuaJIT[1],一個Lua的即時編譯器,由Mike Pall開發(fā)。他所作的工作非常卓越,而且LuaJIT可能是所有動態(tài)語言里最快的JIT了。使用它的代價是它只能在x86架構上運行,而且你需要一個非標準的Lua解釋器(LuaJIT)來運行你的程序。所獲得的好處是你可以在不修改代碼的情況下讓程序的運行速度提高到原先的5倍。第二個方式是將部分代碼移到C中實現(xiàn)。這一條的重點在于為C代碼選擇合適的粒度。一方面,如果你把一些非常簡單的函數(shù)移動到C里,Lua和C之間的通訊開銷會抵消使用C編寫函數(shù)帶來的性能優(yōu)勢;另一方面,如果你把太大的函數(shù)移到C里,你又失去了Lua所提供的靈活性。最后,還要注意的是這兩種方式有時候是不兼容的。你把越多的代碼移到C里,LuaJIT所能帶來的優(yōu)化就越少。