[轉]Lua和Lua JIT及優化指南

1、什麼是lua&luaJit

lua(www.lua.org)其實就是爲了嵌入其它應用程序而開發的一個腳本語言,
luajit(www.luajit.org)是lua的一個Just-In-Time也就是運行時編譯器,也能夠說是lua的一個高效版。html

2、優點

1)lua是一個免費、小巧、簡單、強大、高效、輕量級的嵌入式的腳本語言,lua當前的發行版本5.3.1只有276k。
2)它是用C語言開發的項目,因此能夠在大部分的操做系統上運行
3)lua是目前速度最快的腳本語言,既能夠提高語言的靈活性還能夠最大限度的保留速度
4)其語法很是簡單,沒有特例
5)lua還能夠做爲C的API來使用python

3、不足和不一樣

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才能夠ios

4、綜述

綜上,lua是一個簡單、高效的語言,因此在遊戲邏輯開發和服務器開發中(ngx_lua)獲得普遍的應用。c++

luajit官方性能優化指南和註解

luajit官方性能優化指南和註解編程

luajit是目前最快的腳本語言之一,不過深刻使用就很快會發現,要把這個語言用到像宣稱那樣高性能,並非那麼容易。實際使用的時候每每會發現,剛開始寫的一些小test case性能很是好,常常毫秒級就算完,但是代碼複雜度一上去了,動輒幾十上百毫秒的狀況就會出現,性能表現很是飄忽。數組

爲此luajit的mailling list也是有很多人諮詢,做者mike pall的一篇比較完整的回答被放在了官方wiki上:緩存

http://wiki.luajit.org/Numerical-Computing-Performance-Guideruby

不過原文說了不少怎麼作,卻基本沒有解釋爲何。性能優化

因此這篇文章不是簡單的翻譯官方這個優化指南,最主要仍是讓你們瞭解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 部分排版有修改

相關文章
相關標籤/搜索