轉自: http://www.superyyl.com/?p=104
Lua 性能優化篇(全局與非全局)
在代碼運行前,Lua會把源碼預編譯成一種中間碼,類似於Java的虛擬機。這種格式然后會通過C的解釋器進行解釋,整個過程其實就是通過一個while
循環,里面有很多的switch...case
語句,一個case
對應一條指令來解析。
自Lua 5.0之后,Lua采用了一種類似於寄存器的虛擬機模式。Lua用棧來儲存其寄存器。每一個活動的函數,Lua都會其分配一個棧,這個棧用來儲存函數里的活動記錄。每一個函數的棧都可以儲存至多250個寄存器,因為棧的長度是用8個比特表示的。
有了這么多的寄存器,Lua的預編譯器能把所有的local變量儲存在其中。這就使得Lua在獲取local變量時其效率十分的高。
======================================================================
global he local
舉個栗子: 假設a和b為local變量,a = a + b
的預編譯會產生一條指令:
a是寄存器0, b是寄存器1 ADD 0 0 1
但是若a和b都聲明為非local變量,則預編譯會產生如下指令:
GETGLOBAL 0 0 ;get a GETGLOBAL 1 1 ;get b ADD 0 0 1 ;do add SETGLOBAL 0 0 ;set a
所以你懂的:在寫Lua代碼時,你應該盡量使用local變量。
以下是幾個對比測試,你可以復制代碼到你的編輯器中,進行測試。
采用 a = os.clock()
a = os.clock() for i = 1,10000000 do local x = math.sin(i) end b = os.clock() print(b-a) -- 1.113454
把math.sin
賦給local變量sin
:
a = os.clock() local sin = math.sin for i = 1,10000000 do local x = sin(i) end b = os.clock() print(b-a) --0.75951
直接使用math.sin
,耗時1.11秒;使用local變量sin
來保存math.sin
,耗時0.76秒。可以獲得30%的效率提升!
string 優化篇,用table模擬buffer
與其他主流腳本語言不同的是,Lua在實現字符串類型有兩方面不同。
第一,所有的字符串在Lua中都只儲存一份拷貝。當新字符串出現時,Lua檢查是否有其相同的拷貝,若沒有則創建它,否則,指向這個拷貝。這可以使得字符串比較和表索引變得相當的快,因為比較字符串只需要檢查引用是否一致即可;但是這也降低了創建字符串時的效率,因為Lua需要去查找比較一遍。
第二,所有的字符串變量,只保存字符串引用,而不保存它的buffer。這使得字符串的賦值變得十分高效。例如在Perl中,$x = $y
,會將$y的buffer整個的復制到$x的buffer中,當字符串很長時,這個操作的代價將十分昂貴。而在Lua,同樣的賦值,只復制引用,十分的高效。
但是只保存引用會降低在字符串連接時的速度。在Perl中,$s = $s . 'x'
和$s .= 'x'
的效率差距驚人。前者,將會獲取整個$s的拷貝,並將’x’添加到它的末尾;而后者,將直接將’x’插入到$x的buffer末尾。
---------------------------------------------------------------------------
由於后者不需要進行拷貝,所以其效率和$s的長度無關,因為十分高效。
在Lua中,並不支持第二種更快的操作。以下代碼將花費6.65秒:
a = os.clock() local s = '' for i = 1,300000 do s = s .. 'a' end b = os.clock() print(b-a) --6.649481
我們可以用table來模擬buffer,下面的代碼只需花費0.72秒,9倍多的效率提升:
a = os.clock() local s = '' local t = {} for i = 1,300000 do t[#t + 1] = 'a' end s = table.concat( t, '') b = os.clock() print(b-a) --0.07178
所以:在大字符串連接中,我們應避免..。應用table來模擬buffer,然后concat得到最終字符串。
===========================================================
3R原則
3R原則(the rules of 3R)是:減量化(reducing),再利用(reusing)和再循環(recycling)三種原則的簡稱。
3R原則本是循環經濟和環保的原則,但是其同樣適用於Lua。
Reducing
有許多辦法能夠避免創建新對象和節約內存。例如:如果你的程序中使用了太多的表,你可以考慮換一種數據結構來表示。
舉個栗子。 假設你的程序中有多邊形這個類型,你用一個表來儲存多邊形的頂點:
polyline = { { x = 1.1, y = 2.9 }, { x = 1.1, y = 3.7 }, { x = 4.6, y = 5.2 }, ... }
以上的數據結構十分自然,便於理解。但是每一個頂點都需要一個哈希部分來儲存。如果放置在數組部分中,則會減少內存的占用:
polyline = { { 1.1, 2.9 }, { 1.1, 3.7 }, { 4.6, 5.2 }, ... }
一百萬個頂點時,內存將會由153.3MB減少到107.6MB,但是代價是代碼的可讀性降低了。
最變態的方法是:
polyline = { x = {1.1, 1.1, 4.6, ...}, y = {2.9, 3.7, 5.2, ...} }
一百萬個頂點,內存將只占用32MB,相當於原來的1/5。你需要在性能和代碼可讀性之間做出取舍。
在循環中,我們更需要注意實例的創建。
for i=1,n do local t = {1,2,3,'hi'} --執行邏輯,但t不更改 ... end
我們應該把在循環中不變的東西放到循環外來創建:
local t = {1,2,3,'hi'} for i=1,n do --執行邏輯,但t不更改 ... end
Reusing
如果無法避免創建新對象,我們需要考慮重用舊對象。
考慮下面這段代碼:
local t = {} for i = 1970, 2000 do t[i] = os.time({year = i, month = 6, day = 14}) end
{year = i, month = 6, day = 14}
,但是只有
year
是變量。
下面這段代碼重用了表:
local t = {} local aux = {year = nil, month = 6, day = 14} for i = 1970, 2000 do aux.year = i; t[i] = os.time(aux) end
另一種方式的重用,則是在於緩存之前計算的內容,以避免后續的重復計算。后續遇到相同的情況時,則可以直接查表取出。這種方式實際就是動態規划效率高的原因所在,其本質是用空間換時間。
Recycling
Lua自帶垃圾回收器,所以我們一般不需要考慮垃圾回收的問題。
了解Lua的垃圾回收能使得我們編程的自由度更大。
Lua的垃圾回收器是一個增量運行的機制。即回收分成許多小步驟(增量的)來進行。
頻繁的垃圾回收可能會降低程序的運行效率。
我們可以通過Lua的collectgarbage
函數來控制垃圾回收器。
collectgarbage
函數提供了多項功能:停止垃圾回收,重啟垃圾回收,強制執行一次回收循環,強制執行一步垃圾回收,獲取Lua占用的內存,以及兩個影響垃圾回收頻率和步幅的參數。
對於批處理的Lua程序來說,停止垃圾回收collectgarbage("stop")
會提高效率,因為批處理程序在結束時,內存將全部被釋放。
對於垃圾回收器的步幅來說,實際上很難一概而論。更快幅度的垃圾回收會消耗更多CPU,但會釋放更多內存,從而也降低了CPU的分頁時間。只有小心的試驗,我們才知道哪種方式更適合。
結語
我們應該在寫代碼時,按照高標准去寫,盡量避免在事后進行優化。
如果真的有性能問題,我們需要用工具量化效率,找到瓶頸,然后針對其優化。當然優化過后需要再次測量,查看是否優化成功。
在優化中,我們會面臨很多選擇:代碼可讀性和運行效率,CPU換內存,內存換CPU等等。需要根據實際情況進行不斷試驗,來找到最終的平衡點。
最后,有兩個終極武器:
第一、使用LuaJIT,LuaJIT可以使你在不修改代碼的情況下獲得平均約5倍的加速。查看LuaJIT在x86/x64下的性能提升比。
第二、將瓶頸部分用C/C++來寫。因為Lua和C的天生近親關系,使得Lua和C可以混合編程。但是C和Lua之間的通訊會抵消掉一部分C帶來的優勢。
注意:這兩者並不是兼容的,你用C改寫的Lua代碼越多,LuaJIT所帶來的優化幅度就越小。