1. 程式人生 > >Lua效能優化技巧

Lua效能優化技巧

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

相關推薦

Lua效能優化技巧

前言 和在所有其他程式語言中一樣,在Lua中,我們依然應當遵循下述兩條有關程式優化的箴言: 原則1:不要做優化。 原則2:暫時不要做優化(對專家而言)。 這兩條原則對於Lua程式設計來說尤其有意義,Lua正是因其效能而在指令碼語言中鶴立雞群。 當然,我們

Java效能優化技巧集錦

一、通用篇 “通用篇”討論的問題適合於大多數Java應用。   1.1 不用new關鍵詞建立類的例項 用new關鍵詞建立類的例項時,建構函式鏈中的全部建構函式都會被自己主動呼叫。但假設一個物件實現了Cloneable介面。我們能夠呼叫它的clone()方法。 clone

【Egret優化分享】白鷺引擎王澤:重度H5遊戲效能優化技巧

本文轉自:https://mp.weixin.qq.com/s/GIzXA51D7_hMqajCRuJE2g 9月15日,無懼17級颱風“山竹”,320名開發者齊聚廣州貝塔空間共同探討“怎樣做一款賺錢的小遊戲”。針對眾多開發者關心的重度H5遊戲效能優化技巧,我們整理了現場速記分享給

微信小程式效能優化技巧

摘要: 如果小程式不夠快,還要它幹嘛? 原文:微信小程式效能優化方案——讓你的小程式如此絲滑 作者:杜俊成要好好學習 Fundebug經授權轉載,版權歸原作者所有。 微信小程式如果想要優化效能,有關鍵性的兩點: 提高載入效能 提高渲染效能 接下來分別來介紹一下: 提高載

Java程式設計效能優化技巧,乾貨分享!

Java程式設計效能優化技巧,乾貨分享! 此時靜態變數b的生命週期與A類同步,如果A類不會解除安裝,那麼b物件會常駐記憶體,直到程式終止。 3儘量避免過多過常的建立Java物件 儘量避免在經常呼叫的方法,迴圈中new物件,由於系統不僅要花費時間來建立物件,而且還要花時間對這些

白鷺引擎王澤:重度H5遊戲效能優化技巧

9月15日,無懼17級颱風“山竹”,320名開發者齊聚廣州貝塔空間共同探討“怎樣做一款賺錢的小遊戲”。針對眾多開發者關心的重度H5遊戲效能優化技巧,我們整理了現場速記分享給大家,詳見下文: 王澤:各位開發者下午好!我叫王澤,是白鷺引擎的首席架構師。 今天給大家分享的

PLSQL效能優化技巧

1、理解執行計劃 1-1.什麼是執行計劃     oracle資料庫在執行sql語句時,oracle的優化器會根據一定的規則確定sql語句的執行路徑,以確保sql語句能以最優效能執行.在oracle資料庫系統中為了執行sql語句,oracle可能需要實現多個步驟,這

JavaScript 效能優化技巧分享

JavaScript 作為當前最為常見的直譯式指令碼語言,已經廣泛應用於 Web 應用開發中。為了提高Web應用的效能,從 JavaScript 的效能優化方向入手,會是一個很好的選擇。 本文從載入、上下文、解析、編譯、執行和捆綁等多個方面來講解 JavaScript 的效能優化技巧,以便讓更多的前端開發人

Python 程式碼效能優化技巧

程式碼優化能夠讓程式執行更快,它是在不改變程式執行結果的情況下使得程式的執行效率更高,根據 80/20 原則,實現程式的重構、優化、擴充套件以及文件相關的事情通常需要消耗 80% 的工作量。優化通常包含兩方面的內容:減小程式碼的體積,提高程式碼的執行效率。 一、改進演算法,選擇合適的資料

Spark商業案例與效能調優實戰100課》第3課:商業案例之通過RDD分析大資料電影點評系各種型別的最喜愛電影TopN及效能優化技巧

Spark商業案例與效能調優實戰100課》第3課:商業案例之通過RDD分析大資料電影點評系各種型別的最喜愛電影TopN及效能優化技 原始碼 package com.dt.spark.core

elasticsearch-索引效能優化技巧

段和合並編輯 段合併的計算量龐大, 而且還要吃掉大量磁碟 I/O。合併在後臺定期操作,因為他們可能要很長時間才能完成,尤其是比較大的段。這個通常來說都沒問題,因為大規模段合併的概率是很小的。 不過有時候合併會拖累寫入速率。如果這個真的發生了,Elasticsearch 會自動限制索引請求到單

Unity UI效能優化技巧

問題:UI Canvas上有一個或多個元素變化時,會汙染整個畫布。 畫布(Canvas)是Unity UI的基本元件。它會生成網格來呈現放置在畫布上的UI元素,當UI元素變化時,它會重新生成網格並向GPU發起繪圖呼叫,從而顯示出UI。 生成這些網格會消耗大量效能,需要將U

Lua】提升lua效能技巧

儘量使用local,區域性變數的訪問會比全域性變數快很多(外部區域性變數也比全域性變數快不 避免在程式中編譯程式碼(將字串編譯為程式碼,如 loadstring(string.format("r

lua效能優化之luajit整合

luajit工作模式:luajit中存在兩種工作模式,分別如下: 1.jit模式:也就是即時編譯(just in time)模式。該模式下會將程式碼直接翻譯成機器碼,並向作業系統申請可執行記憶體空間來儲存轉換後的機器碼。執行時直接執行機器碼就行,所以效率是最高

PL/SQL效能優化技巧

1、理解執行計劃 1-1.什麼是執行計劃     Oracle資料庫在執行sql語句時,oracle的優化器會根據一定的規則確定sql語句的執行路徑,以確保sql語句能以最優效能執行.在oracle資料庫系統中為了執行sql語句,oracle可能需要實現多個步

效能優化技巧

1.更改冷啟動白屏的問題,在appliciton中設定啟動 <style name="Theme.AppCompat.Light.NoActionBar"> <

Hibernate查詢效能優化技巧

4.3 使用HQL查詢 Hibernate提供了異常強大的查詢體系,使用Hibernate有多種查詢方式。可以選擇使用Hibernate的HQL查詢,或者使用條件查詢,甚至可以使用原生的SQL查詢語句,此外還提供了一種資料過濾功能,這些都可用於篩選目標資料。 下面分別介紹Hibernate

Hibernate效能優化技巧

一、在處理大資料量時,會有大量的資料緩衝儲存在Session的一級快取中,這快取大太時會嚴重顯示效能,所以在使用Hibernate處理大資料量的,可以使用session. clear()或者session. evict(Object) 在處理過程中,清除全部的快取或者清除某

idea效能優化,使用小技巧

更多學習文章和資源請關注公眾號:Java程式設計指南 IDEA 配置優化,提高開發效率 去掉煩人的indent提示### 如何去掉呢? 開啟IDEA 的preferences|Editor|Code Style, 去掉下圖中的兩個勾選:   設定檔案的模板###

Java程式效能優化—十年碼農總結的程式設計小技巧

程式的效能受程式碼質量的直接影響。在本文中,主要介紹一些程式碼編寫的小技巧和慣例,這些技巧有助於在程式碼級別上提升系統性能。 1、慎用異常 在Java軟體開發中,經常使用 try-catch 進行錯誤捕獲,但是,try-catch 語句對系統性能而言是非常糟糕的。雖然在一次 try-catc