一、什么是lua&luaJit
lua(www.lua.org)其實就是為了嵌入其它應用程序而開發的一個腳本語言,
luajit(www.luajit.org)是lua的一個Just-In-Time也就是運行時編譯器,也可以說是lua的一個高效版。
二、優勢
1)lua是一個免費、小巧、簡單、強大、高效、輕量級的嵌入式的腳本語言,lua當前的發行版本5.3.1只有276k。
2)它是用C語言開發的項目,所以可以在大部分的操作系統上運行
3)lua是目前速度最快的腳本語言,既可以提升語言的靈活性還可以最大限度的保留速度
4)其語法非常簡單,沒有特例
5)lua還可以作為C的API來使用
三、不足和不同
1)lua沒有強大的庫,所以很多功能實現起來沒有python、perl、ruby等腳本語言簡潔
2)lua的異常處理功能飽受爭議,雖然其提供了pcall和xpcall的異常處理函數
3)lua原生語言中沒有提供對unicode編碼的支持,雖然可以通過一些折中的辦法實現 http://www.cppblog.com/darkdestiny/archive/2009/04/25/81055.html
4)沒有提供在C++中應用很廣泛的a?b:c的三元運算符操作
5)沒有switch...case...語法,只能通過if..elseif..elseif..else..end的方式折中實現
6)在循環時沒有提供continue語法
7)沒有C++中應用廣泛的a++和a+=1等操作
8)lua的索引是從1開始的,而不是我們熟悉的0(string,table)
9)當你給一個元素賦值為nil時相當於這個元素不存在
10)lua的數值類型只有number是沒有int,float,double等之分的
11)lua中沒有類的概念,其類是通過table的形式來實現的
12)lua中只有nil和false是表示假的,零在lua中是為真的
13)很多程序需要()標示才能運行,比如a={["b"]=5},print(a.b)是可運行的,但是 {["b"]=5}.b就會報錯,需要({["b"]=5}).b才可以
四、綜述
綜上,lua是一個簡單、高效的語言,所以在游戲邏輯開發和服務器開發中(ngx_lua)得到廣泛的應用。
luajit官方性能優化指南和注解
luajit是目前最快的腳本語言之一,不過深入使用就很快會發現,要把這個語言用到像宣稱那樣高性能,並不是那么容易。實際使用的時候往往會發現,剛開始寫的一些小test case性能非常好,經常毫秒級就算完,可是代碼復雜度一上去了,動輒幾十上百毫秒的情況就會出現,性能表現非常飄忽。
為此luajit的mailling list也是有不少人咨詢,作者mike pall的一篇比較完整的回答被放在了官方wiki上:
http://wiki.luajit.org/Numerical-Computing-Performance-Guide
不過原文說了很多怎么做,卻基本沒有解釋為什么。
所以這篇文章不是簡單的翻譯官方這個優化指南,最主要還是讓大家了解luajit背后的一些原理,因為原文中只有告訴你怎么做,卻沒說清楚為什么,導致做了這些優化,到底影響多大,原因是啥,十分模糊。了解背后的原因往往對我們有很大的幫助。
另外,原生lua、luajit的jit模式(pc和安卓可用)、luajit的interpreter模式(ios下只能運行這個),他們執行lua的原理是有很大的不同的,也導致一些lua優化技巧並不見得是通用的。而這篇文章主要針對luajit的jit模式。
1.Reduce number of unbiased/unpredictable branches.
減少不可預測的分支代碼
分支代碼就是根據條件會跳轉的代碼(最典型就是if..else),那什么是不可預測的分支代碼?簡單說:
if 條件1 then
elseif 條件2 then
假如條件1或者條件2其中一方達成的概率非常高(>95%),那我們認為這是可預測的分支代碼。
這是被mike pall放到第一位的性能優化點(事實上確實應該如此),究其原因是luajit使用了trace compiler的特性,為了生成的機器碼盡可能高效,它會根據代碼的運行情況進行一些假設,比如上面的例子如果luajit發現,條件2的達成概率非常高,那么luajit會生成按條件2達成執行最快的代碼。
有一點可能大家會問,luajit真的能知道運行過程中的一些情況?
是的
這也是trace compiler的特征:先運行字節碼,針對熱點代碼做profile,了解了可以優化的點后再優化出最高效的機器碼。這就是luajit目前的做法。
為什么要這樣呢?給一個比較好理解的例子:luajit是動態類型語言,面對一個a+b,你根本不知道a和b是什么類型,如果a+b只是兩個整數相加,那么編譯機器碼做求和速度自然是飛快的。可是如果你無法確認這點,結果你只能假定它是任意類型,先去動態檢查類型(看看到底是兩個表,還是兩個數值,甚至是其他情況),再跳根據類型做相應的處理,想想都知道比兩個整數相加慢了幾十倍。
所以luajit為了極限級的性能,就會大膽進行假設,如果發現a+b就是兩個數值相加,就編譯出數值求和的機器碼。
但是如果某一時刻a+b不是數值相加,而是變成了兩個表相加呢?這機器碼豈不是就導致錯誤了?因此每次luajit做了假設時,都會加上一段守護代碼(guard),檢查假設是不是對的,如果不對,就會跳轉出去,再根據情況,來決定要不要再編譯一段新的機器碼,來適配新的情況。
這就是為什么你的分支代碼一定要可預測,因為如果經常不符合luajit假設的東西,就會經常從編譯好的機器碼中跳出來,甚至會因為好幾次假設失敗而連跳好幾次。所以,luajit是一個對分支情況極度敏感的語言。
這是luajit的第一性能大坑,作者建議可以借助math.min/max或者bitop來繞過if else這樣的分支代碼。不過實際情況往往更復雜,所有涉及到跳轉代碼的地方,都是潛在的性能坑。
另外,在interpreter模式下(ios的情況),luajit就變成了老老實實動態檢查動態跳轉的執行模式,對分支預測反而並不敏感,並不需要過分注重這方面的優化。
2.Use FFI data structures.
如果可以,將你的數據結構用ffi實現,而不是用lua table實現
luajit的ffi是一個常被大家忽略的功能,或者只被當做一個更好用的c導出庫,但事實上這是一個超級性能利器。
比如要實現unity中的Vector3,分別用lua table和用ffi實現,我們測試下來,內存占用是10:1,運算x+y+z的耗時也是大概8:1,優化效率驚人。
代碼如下:
local ffi = require("ffi")
ffi.cdef [[
typedef struct { float x, y, z; } vector3c;
]]
local count = 100000
local function test1()
local startTime = os.time()
-- lua table的代碼
local vecs = {}
for i = 1, count do
vecs[i] = { x = 1, y = 2, z = 3 }
end
local total = 0
-- gc后記錄下面for循環運行時的時間和內存占用,這里省略
for i = 1, count do
total = total + vecs[i].x + vecs[i].y + vecs[i].z
end
local diffTime = os.time() - startTime
print("lua table 耗時:", diffTime)
end
local function test2()
local startTime = os.time()
-- ffi的代碼
local vecs = ffi.new("vector3c[?]", count)
for i = 1, count do
vecs[i] = { x = 1, y = 2, z = 3 }
end
local total = 0
-- gc后記錄下面for循環運行時的時間和內存占用,這里省略
for i = 1, count do
total = total + vecs[i].x + vecs[i].y + vecs[i].z
end
local diffTime = os.time() - startTime
print("ffi 耗時:", diffTime)
end
test1()
test2()
為何有這么大的差距?因為lua table本質是一個hash table,在hash table訪問字段固然是緩慢的並且要存儲大量額外的東西。而ffi可以做到只分配xyz三個float的空間就能表示一個Vector3,自然內存占用要低得多,而且jit會利用ffi的信息,實現訪問xyz的時候直接讀內存,而不是像hash table那樣走一次key hash,性能也高得多。
不幸的是ffi只在有jit模式的時候才能有很好的運行速度,現在做手游基本都要做ios,而ios下由於只能運行解釋模式,ffi的性能很差(比純table反而更慢),僅僅內存優勢得到保留,所以如果要考慮ios這樣的平台,這個優化點基本可以忽略,或者只在安卓下針對少數核心代碼進行優化。
3.Call C functions only via the FFI.
盡可能用ffi來調用c函數。
同樣的,ffi也可以用於調用已經extern c的c函數。大家表面上都以為這樣做只是省掉了用tolua之類的工具做導出的麻煩,但ffi更大的好處,是在於性能上質的提升。
這是因為,使用ffi導出c函數,你需要提供c函數的原型,有了c函數的原型信息,luajit可以知道每個參數的准確類型,返回值的准確類型。了解編譯器知識的同學都知道函數調用和返回一般都是用棧來實現的,而要做到這點必須要知道整個參數列表和返回值類型,才能生成出出棧入棧的代碼。因此luajit在擁有這些信息之后就可以生成機器碼,跟c編譯器一樣做到無縫的調用,而不需要像標准的lua與c交互那樣需要調用pushint等等函數來傳參了。
如果不通過ffi調用c導出函數,那么因為luajit缺乏這個函數的信息,無法生成用於調用c函數的jit代碼,自然會降低性能。而且在2.1.0版本之前,這會直接導致jit失敗,整段相關的代碼都無法jit化,性能會收到極大的影響。
4.Use plain 'for i=start,stop,step do ... end' loops.
實現循環時,最好使用簡單的for i = start, stop, step do這樣的寫法,或者使用ipairs,而盡量避免使用for k,v in pairs(x) do
首先,直到目前最新的luajit2.1.0beta2,for k,v in pairs(t) do end是不支持jit的(即無法生成機器碼運行)。至於這個坑的存在主要還是因為按kv遍歷table的匯編比較難寫,但至少可以知道,目前如果想高效遍歷數組或者做for循環,直接使用數值做索引是最佳的方法。
其次,這樣的寫法更利於做循環展開。
5.Find the right balance for unrolling.
循環展開,有利有弊,需要自己去平衡
在早期的c++時代,手工將循環代碼展開成順序代碼是一種常見的優化方法,但是后來編譯器都集成了一定的循環展開優化能力,代替手工做這種事情。而luajit本身也帶有這塊的優化(可以參考其實現函數lj_opt_loop),可以對循環進行展開。
不過這個展開是在運行時做的,所以也有利有弊。作者舉例,如果在一個兩層循環中,內循環的循環次數不夠10次,這個部分會被嘗試展開,但是由於嵌套在外部的大循環,外部大循環可能會導致內部循環多次進入,多次展開,導致展開次數過大,最終jit會取消展開。
至於這方面的性能未做深入測試,作者也只是給出了一些比較感性的優化建議(最后來了一句,You may have to experiment a bit),有了解的同學歡迎交流。
6.Define and call only 'local' (!) functions within a module.
7.Cache often-used functions from other modules in upvalues.
這兩點都可以拿到一起說,即調用任何函數的時候,保證這個函數是local function,性能會更好,比如:
local ms = math.sin
function test()
math.sin(1)
ms(1)
end
這兩行調用math.sin有什么區別呢?
事實上math是一個表,math.sin本身就做了一次表查找,key是sin,這里消耗了一次。而math又是一個全局變量,那還要在全局表中做一次查找(_G[math])
而local ms緩存過之后,math.sin查找就可以省掉了,另外,對於function上一層的變量,lua會有一個upvalue對象進行存儲,在找ms這個變量的時候就只需要在upvalue對象內找,查找范圍更小更快捷
當然,jit化后的代碼有可能會進一步優化這個過程,但是更好的辦法依然是自行local緩存
總之,如果某個函數只在本文件內用到,就將其local,如果是一個全局函數,用之前用local緩存一下。
8.Avoid inventing your own dispatch mechanisms.
避免使用你自己實現的分發調用機制,而盡量使用內建的例如metatable這樣的機制
編程的時候為了結構優雅,常常會引入像消息分發這樣的機制,然后在消息來的時候根據我們給消息定義的枚舉來調用對應的實現,過去我們也習慣寫成:
if opcode == OP_1 then
elesif opcode == OP_2 then
...
但在luajit下,更建議將上面實現成table或者metatable
local callbacks = {}
callbacks[OP_1] = function() ... end
callbacks[OP_2] = function() ... end
這是因為表查找和metatable查找都是可以參與jit優化的,而自行實現的消息分發機制,往往會用到分支代碼或者其他更復雜的代碼結構,性能上反而不如純粹的表查找+jit優化來得快
9.Do not try to second-guess the JIT compiler.
無需過多去幫jit編譯器做手工優化。
作者舉了一個例子
z = x[a+b] + y[a+b],這在luajit是性能ok的寫法,不需要先local c = a+b然后z = x[c] + y[c]
后面的寫法其實本身沒什么問題,但是luajit的另一個坑,即為了提升運算效率,local變量會盡可能用cpu寄存器存儲,這樣比頻繁讀內存要快得多(現代cpu這可以達到幾百倍的差距),但luajit在這方面不完善,一旦local變量太多,可能會找不到足夠的寄存器分配(這個問題在armv7上非常明顯,在調用層次深的時候,幾個變量就會炸掉),然后jit會直接放棄編譯。這里要說明一點是,很多local變量可能只是聲明了放在那里沒有用,但是luajit的編譯器不一定能夠准確確定這個變量是否可以不再存儲,所以適當控制一個函數作用域內的local變量的數量是必須的。
當然,不得不說這樣寫代碼還要猜luajit的行為確實比較痛苦,一般來說進行profile然后對性能熱點代碼做針對測試和優化基本已經可以。
10.Be careful with aliasing, esp. when using multiple arrays.
變量的別名可能會阻止jit優化掉子表達式,尤其是在使用多個數組的時候。
作者舉了一個例子
x[i] = a[i] + c[i]; y[i] = a[i] + d[i]
我們可能會認為兩a[i]是同一個東西,編譯器可以優化成
local t = a[i]; x[i] = t + c[i]; y[i] = t + d[i]
實則不然,因為可能會出現,x和a就是同一個表,這樣,x[i] = a[i] + c[i]就改變了a[i]的值,那么y[i] = a[i] + d[i]就不能再使用之前的a[i]的值了
這里跟優化點9描述的情形的本質區別是,優化點9里頭z/a/b都是值類型,而這里x/a都是引用類型,引用類型就有引用同一個東西的可能(變量別名),因此編譯器會放棄這樣的優化。
11.Reduce the number of live temporary variables.
減少存活着的臨時變量的數量
原因在9中已經說明,即過多的存活着的臨時變量可能會耗盡寄存器導致jit編譯器無法利用寄存器做優化。這里注意live temporary variables是指存活的臨時變量,假如你提前結束了臨時變量的生命周期,編譯器還是會知道這一點的。比如:
function foo()
do
local a = "haha"
end
print(a)
end
這里print是會print出nil,因為a離開了do ... end就結束了生命周期,通過這種方式可以避免過多臨時變量同時存活。
此外,有一個很常見的陷阱,例如我們實現了一個Vector3的類型用於表達立體空間中的矢量,常常會重載他的一些元函數,比如__add
Vector3.__add = function(va, vb)
return Vector3.New(va.x + vb.x, va.y + vb.y, va.z + vb.z)
end
然后我們就會在代碼中大肆地使用a + b + c來對一堆的Vector3做求和運算。
這其實對luajit是有很大的隱患的,因為每個+都產生了一個新的Vector3,這將會產生大量的臨時變量,且不考慮這里的gc壓力,光是為這些變量分配寄存器就已經十分容易出問題。
所以這里最好在性能和易用性上進行權衡,每次求和如果是將結果寫會到原來的表中,那么壓力會小很多,當然代碼的易用性和可讀性上就可能要犧牲一些。
12.Do not intersperse expensive or uncompiled operations.
減少使用高消耗或者不支持jit的操作
這里要提到一個luajit文檔中的屬於:NYI(not yet implement),意思就是,作者還沒有把這個功能做完。。
luajit快是快在能把代碼編譯為機器碼執行,但是並非所有代碼都可以jit化,除了前面提到的for in pairs外,還有很多這樣的東西,最常見的有:
for k, v in pairs(x):主要是pairs是無jit實現的,盡可能用ipairs替代。
print():這個是非jit化的,作者建議用io.write。
字符串連接符:打日志很容易會寫log("haha "..x)這樣的方式,然后通過屏蔽log的實現來避免消耗。事實上真的可以屏蔽掉嗎?然並卵。因為"haha"..x這個字符串鏈接依然會被執行。在2.0.x的時候這個代碼還不支持jit,2.1.x雖然終於支持了,但是多余的連接字符串運算以及內存分配依然發生了,所以想要屏蔽,可以用log("haha %s", x)這樣的寫法。
table.insert:目前只有從尾部插入才是jit實現的,如果從其他地方插入,會跳轉到c實現。
關於在Unity中提升Lua的性能,參考文章:https://www.cnblogs.com/zwywilliam/
本文轉自:https://blog.csdn.net/qq_35624156/article/details/77455670 部分排版有修改