原文:Lua Performance Tips程序員
像其餘任何編程語言同樣,在Lua中,咱們也要遵照如下兩條優化程序的規則:算法
規則1:不要優化。編程
規則2:仍然不要優化(專家除外)小程序
當用Lua編程時,這兩條規則顯得尤其重要。Lua以性能著稱,並且在腳本語言中也所以而值得讚美。windows
然而,咱們都知道性能是編程的一個關鍵因素。具備複雜指數時間的問題被稱做疑難問題並非偶然發生。太遲的結果是無用的結果。所以,每一個優秀的程序員應該老是在花費資源去優化一段代碼的代價和這段代碼在運行代碼時節約資源的收益相平衡。一個優秀的程序員關於優化的第一個問題老是會問:「程序須要優化嗎?」若是答案是確定的(僅當此時),第二個問題應該是:「哪地方?」數組
爲了回答這兩個問題咱們須要些手段。咱們不該該在沒有合適的測量時嘗試優化軟件。大牛和菜鳥以前的不一樣不是有經驗的程序員更好的指出程序的一個地方可能耗時:不一樣之處是大牛知道他們並不擅長那項任務。數據結構
最近幾年,Noemi Rodriguez和我用Lua開發了一個CORBA ORB(Object Request Broker)原型,後來進化成OiL(Orb in Lua)。做爲第一個原型,以執行簡明爲目標。爲了不引用額外的C語言庫,這個原型用一些計算操做分離每一個字節(轉化成256的基數)。不支持浮點數。由於CORBA把字符串做爲字符序列處理,咱們的ORB第一次把Lua的字符串轉化成字符序列(是Lua中的table),而後像其餘序列那樣處理結果。閉包
當咱們完成第一個原型,咱們和用C++實現的專業的ORB的性能相比較。咱們預期咱們的ORB會稍微慢點,由於它是用Lua實現的,可是,慢的太讓咱們失望了。開始時,咱們只是歸咎於Lua。最後,咱們猜測緣由多是每一個數字序列化所須要的那些操做。所以,咱們決定在分析器下下運行程序。咱們用了一個很是簡單的分析器,像《Programming in Lua》第23章描述的那樣。分析器的結果震驚到咱們。和咱們的直覺不一樣,數字序列化對性能的影響不大,由於沒有太多的數字序列化。然而,字符串序列化佔用總時間的很大一部分。實際上每一個CORBA消息都有幾個字符串,即便咱們不明確地操做字符串:對象引用,方法名字和其餘的某些整數值都被編碼成字符串。而且每一個字符串序列化須要昂貴的代價去操做,由於這須要建立新表,用每一個單獨的字符填充,而後序列化這些結果的順序,這涉及到一個接一個序列化每一個字符。一旦咱們從新實現字符串序列化做爲特殊的事件(替換使用通常的序列代碼),咱們就能獲得可觀的速度提高。僅僅用額外的幾行代碼,你的執行效率就能比得上C++的執行(固然,咱們的執行仍然慢,但不是一個數量級)。架構
所以,當優化程序性能時,咱們應老是去測量。測量前,知道優化哪裏。測量後,知道所謂的「優化」是否真正的提升了咱們的代碼。編程語言
一旦你決定確實必須優化你的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語言解釋。
可能在某些地方你已經讀過從5.0版本Lua使用基於寄存器的虛擬機。這個虛擬機的「寄存器」和真正CPU的寄存器不相符,由於這種相符是不能移植而且十份限制可用寄存器的數量。取而代之的是,Lua使用堆(一個數組加上些索引來實現)容納寄存器。每一個活動函數有一個活動記錄,那是個函數在其中存儲其寄存器的堆片斷。所以,每一個函數有他本身的寄存器(這相似於在windows某些CPU建立的寄存器)。每一個函數可能使用超過250個寄存器,所以每一個指令僅有8位引用寄存器。
提供了大量的寄存器,Lua預編譯可以在寄存器儲存剩餘的局部變量。結果是在Lua中訪問局部變量很是快。舉個例子,若是a和b都是局部變量,像a = a + b這種語句生成單條指令:ADD 0 0 1(假設a和b中分別儲存0和1)。做爲比較,若是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)。除非你必須運行動態的代碼,像經過終端輸入的代碼,你不多須要編譯動態代碼。
做爲例子,考慮下面的代碼,建立一個返回1到10000常數值的函數的表:
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秒。
使用閉包,咱們無需動態編譯。下面的代碼用1/10的時間(0.14秒)建立一樣的100000個函數。
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程序),仍是知道Lua是如何執行表的會比較好。
在Lua中表的執行涉及一些聰明的算法。Lua中的表有兩部分:數組和哈希。對某些特殊的n,數組存儲從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
也作相似的 操做,除了表的哈希部分增加外。
對於很大的表,初始化的開銷會分攤到整個過程的建立:雖然有三個元素的表如要三次從新哈希,但有一百萬個元素的表只須要20次。可是當你建立上千個小的表時,總的消耗會很大。
舊版本的Lua建立空表時會預分配幾個位置(4個,若是我沒記錯的話),以免這種初始化小表時的開銷。然而,這種方法會浪費內存。舉個例子,若是你建立一百萬個座標點(表現爲只有兩個元素的表)而每一個使用實際須要的兩倍內存,你所以會付出高昂的代價。這也是如今Lua建立空表不會預分配的緣由。
若是你用C語言編程,你能夠經過Lua的API中lua_createtable函數避免那些從新哈希。他在無處不在的lua_State後接受兩個參數:新表數組部分的初始大小和哈希部分的初始大小。(雖然從新哈希的運算法則總會將數組的大小設置爲2的冪次方,數組的大小能夠是任意值。然而,哈希的大小必須是2的冪次方,所以,第二個參數老是取整爲不比原值小的較小的2的冪次方)經過給出新表合適的大小,這很容易避免那些初始的再哈希。小心,不管如何,Lua只能在再哈希時候才能收縮表。所以,若是你初始大小比須要的大,Lua可能永遠不會糾正你浪費的空間。
當用Lua編程時,你能夠用構造器避免那些初始再哈希。當你寫下{true, true, true}時,Lua會預先知道表的數組部分將會須要上三個空位,所以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須要插入新的元素時候發生。若是你遍歷表清除全部的字段(即設置他們爲空),結果是表不會收縮。然而,若是你插入一些新的元素,最後表不得不從新調整大小。一般這不是個問題:若是你一直清除元素和插入新的(在不少程序中都是有表明性的),表的大小保持不變。然而,你應該不指望經過清除大的表的字段來恢復內存:最好是釋放表自己。
一個強制從新哈希的鬼把戲是插入足夠可能是空值到表中。看接下來的例子:
a = {} lim = 10000000 for i = 1, lim do a[i] = i end -- create a huge table print(collectgarbage("count")) --> 196626 for i = 1, lim do a[i] = nil end -- erase all its elements print(collectgarbage("count")) --> 196626 for i = lim + 1, 2*lim do a[i] = nil end -- create many nil elements print(collectgarbage("count")) --> 17
我不推薦這種鬼把戲,除非在特殊狀況下:這會很慢而且沒有容易的方法指導「足夠」是指多少元素。
你可能會好奇爲何當插入空值時Lua不會收縮表。首先,要避免測試插入表的是什麼;檢測賦空值會致使全部的賦值變慢。其次,更重要的是,當遍歷表時容許賦空值。思考接下來的這個循環:
for k, v in pairs(t) do if some_property(v) then t[k] = nil -- erase that element end end
若是賦空值後Lua對錶從新哈希,這回破壞本次遍歷。
若是你想清空表中全部的元素,一個簡單的遍歷是實現他的正確方法:
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函數開始遍歷表的數組,查找不爲空的元素。當循環設置第一個元素爲空時,next函數花更長的時間查找第一個非空元素。結果是,「聰明」的循環花費20秒清除有100,000個元素的表;使用pairs遍歷循環花費0.04秒。
和表同樣,爲了更高效的使用字符串,最好知道Lua是如何處理字符串的。
不一樣於大多數的腳本語言,Lua實現字符串的方式表如今兩個重要的方面。第一,Lua中全部的字符串都是內化的。意思是Lua對任一字符串只保留一份拷貝。不管什麼時候出現新字符串,Lua會檢測這個字符串是否已經存在備份,若是是,重用拷貝。內化使像字符串的比較和表索引操做很是快,可是字符串的建立會慢。
第二,Lua中的變量從不持有字符串,僅是引用他們。這種實現方式加快了幾個字符串的操做。舉個例子,在Perl語言中,當你寫下相似於$x = $y,$y含有一個字符串,賦值會從$y緩衝中字符串內容複製到$x的緩衝。若是字符串很長的話,這就會變成昂貴的操做。在Lua中,這種賦值只需複製指向字符串的指針。
然而,這種帶有引用實現減慢了字符串鏈接的這種特定形式。在Perl中,$s = $s . "x"和$s . = "x"操做使徹底不同的。在第一個中,你獲得的一個$s的拷貝,並在它的末尾加上「x」。在第二個中,「x」簡單地附加到由$s變量保存的內部緩衝上。所以,第二種形式和字符串的大小不相關(假設緩衝區有多餘文本的空間)。若是你在循環內部用這些命令,他們的區別是線性和二次方算法的區別。舉個例子,下面的循環讀一個5M的文件花費了約5分鐘。
$x = ""; while (<>) { $x = $x . $_; }
若是咱們把 $x = $x . $_ 變成 $x .= $_, 此次時間降低到0.1秒!
Lua不支持第二個,更快的那個,這是由於它的變量沒有緩衝和它們相關聯。所以,咱們必須用顯示的緩衝:字符串表作這項工做。下面的循環0.28秒讀取一樣的5M文件。雖然不如Perl快,但也很不錯了。
local t = {} for line in io.lines() do t[#t + 1] = line end s = table.concat(t, "\n")
當處理Lua資源時,咱們應該一樣用推進地球資源的3R倡議。
簡化是這三個選項中最簡單的。有幾種方法能夠避免對新對象的須要。舉個例子,若是你的程序使用了不少的表,能夠考慮數據表現的改動。舉個簡單的例子,考慮程序操做折線。在Lua中最天然的表示折線是一組點的列表,像這樣:
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"} -- do something without changing ’t’ ... end end local t = {1, 2, 3, "hi"} -- create ’t’ once and for all function foo (...) for i = 1, n do -- do something without changing ’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 -- else return nothing, keeping the original number 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獲得匹配的子字符串。(標準庫有一個比較子字符串的功能是個好主意,以便咱們沒必要從字符串提取出那個值(於是建立了一個新字符串))
當咱們不可避免使用新對象時,經過重用咱們任然能夠避免建立那些新對象。對於字符串的重用是沒有必要的,由於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
一個特別有效的方法來實現複用的方法是經過memoizing.。基本思想很是簡單:儲存輸入的某些計算的結果,以便當再有相同的輸入時,程序只需複用以前的結果。
LPeg,一個Lua中新的模式匹配包,對memoizing的使用頗有意思。LPeg把每一個模式編譯成內在的形式,一個用於解析機器執行匹配的「程序」。這種編譯與匹配自身相比代價很是昂貴。所以,LPeg記下它的編譯結果並複用。一個簡單的表將描述模式的字符串與相應的內部表示相關聯。
memoizing的一般問題是儲存之前結果花費的空間可能超過複用這些結果的收益。Lua爲了解決這個問題,咱們能夠用弱表來保存結果,以便沒有用過的結果最後能從表裏移除。
Lua中,用高階函數咱們能夠定義個通用的memoization函數:
function memoize (f) local mem = {} -- memoizing table setmetatable(mem, {__mode = "kv"}) -- make it weak return function (x) -- new version of ’f’, with memoizing local r = mem[x] if r == nil then -- no previous result? r = f(x) -- calls original function mem[x] = r -- store result for reuse end return r end end
給出任意的函數f,, memoize(f)返回一個新的和f返回相同結果的函數,而且記錄它們。舉個例子,咱們能夠從新定義帶memoizing版本的loadstring:
loadstring = memoize(loadstring)
咱們徹底像以前的那個那樣使用新函數,可是若是咱們加載的字符串中有不少重複的,咱們能得到可觀的收益。
若是你的程序建立和釋放太多的協程,回收再生多是個提升性能的選擇。當前的協程API不提供直接支持複用協程,可是咱們能夠突破這個限制。考慮下面的協程
co = coroutine.create(function (f) while f do f = coroutine.yield(f()) end end
這個協程接受一個做業(運行一個函數),返回它,而且完成後等待下一個做業。
Lua中大多數的再生由垃圾回收器自動執行。Lua用一個增量的垃圾回收器。這意味着回收器表現爲以較小的步調(逐步地)與程序執行交錯執行任務。這些步調的節奏正比於內存分配:Lua每分配必定量的內存,垃圾收集器就會作一樣比例的工做。程序消耗內存越快,收集器回收的越快。
若是咱們對程序應用簡化和複用原則,一般收集器沒有太多的工做可作。可是有時咱們不能避免大量垃圾的產生,此時收集器就變的笨重了。Lua中垃圾收集器爲通常程序作了調整,所以在多數軟件中表現的至關不錯。然而,有時對於特殊的狀況經過調整收集器咱們能夠提升程序的性能。
咱們能夠經過Lua中collectgarbage函數或C中的lua_gc控制垃圾收集器。儘管接口不一樣,但二者都提供的功能基本同樣。我會用Lua的接口來討論,可是,一般這種操做用C比較好。
collectgarbage函數提供了幾個功能:它能夠中止和重啓收集器,強制完整的收集循環,強制收集的一步,得到Lua使用的總內存,而且改變影響收集器步幅的兩個參數。當調整內存不足的程序時它們各有用途。
對於某些類型的批處理程序,「永遠」中止收集器是個選擇,它們建立幾個數據結構,基於這些數據結構產生輸出,而後退出(例如編輯器)。對於這些程序,試圖回收垃圾可能浪費時間,由於只有不多的垃圾被回收,而且當程序結束時全部的內存會被釋放。
對於非批處理的程序,永遠中止收集器並不是是個選擇。儘管如此,這些程序可能會收益於在某些關鍵時期中止收集器。若是有必要,程序能夠徹底控制垃圾收集器,作法是一直保持它中止,只有明確地強制一個步驟或一次完整收集來運行它運行。舉個例子,有些事件驅動平臺提供設置idle函數選項,當沒有其餘的事件處理時纔會被調用。這是垃圾回收的絕佳時間。(Lua5.1中,每次當收集器中止時,強制執行某些收集。所以,強制某些收集後你必須當即調用collectgarbage("stop")來保持他們中止。)
最後,做爲最後一個手段,你能夠嘗試更改收集器的參數。收集器有兩個參數控制它的步幅。第一個叫作pause,控制收集器在完成一個收集週期和開始下一個等待多長時間。第二個參數叫作stepmul(來自step multiplier),控制每個步驟收集器收集多少。簡言之,較小的暫停和較大的步幅能提升收集器的速度。
這些參數對程序的整體性能影響是很難預料的。更快的收集器明顯浪費更多的CPU週期;然而,它能減小程序使用的總的內存,從而減小分頁。只有仔細的嘗試才能給你這些參數的最佳值。
正如咱們介紹中討論的那樣,優化是有技巧的。這裏有幾點須要注意,首先程序是否須要優化。若是它有實際的性能問題,那麼咱們必須定位到哪一個地方以及如何優化。
這裏咱們討論的技術既不是惟一也不是最重要的一個。咱們關注的是Lua特有的技術,由於有更多的針對通用技術的資料。
在咱們結束前,我想提兩個在提高Lua程序性能邊緣的選項。由於這兩個都涉及到Lua代碼以外的變化。第一個是使用LUaJIT,Mike Pall開發的Lua即便編譯器。他已經作了出色的工做,而且LuaJIT多是目前動態語言最快的JIT。缺點是,他只能運行在x86架構上,並且,你須要非標準的Lua解釋器(LuaJIT)來運行程序。優勢是在一點也不改變代碼的狀況下能快5倍的速度運行你的程序。
第二個選擇是把部分代碼放到C中。畢竟,Lua的特色之一是與C代碼結合的能力。這種狀況下,最重要的一點是爲C代碼選擇正確的粒度級別。一方面,若是你只把很是簡單的函數移到C中,Lua和C通訊的開銷可能超過那些函數對性能提高的收益。另外一方面,若是你把太大的函數移到C中,又會失去靈活性。
最後,謹記,這兩個選項有點不兼容。程序中更多的C代碼,LuaJIT能優化代碼就會更少。
簡化,複用,再生