lua效能優化之luajit整合
luajit工作模式:luajit中存在兩種工作模式,分別如下:
1.jit模式:也就是即時編譯(just in time)模式。該模式下會將程式碼直接翻譯成機器碼,並向作業系統申請可執行記憶體空間來儲存轉換後的機器碼。執行時直接執行機器碼就行,所以效率是最高的。但是iOS,xbox,ps4等平臺鑑於自身安全原因都是不授權分配可執行記憶體空間的,所以這些平臺下就不能使用jit模式。
2.interpreter模式:也就是翻譯器模式。該模式下會將程式碼先翻譯成位元組碼,然後將位元組碼翻譯成機器碼,所以無需像作業系統申請可執行記憶體空間,所以幾乎所有平臺都支援這種模式。但是效能相比jit模式而言還是有一定差距的。
luajit的工作方式:luajit採用trace compiler方案,也就是追蹤編譯方案。luajit都會先用interpreter模式將程式碼轉換成位元組碼。然後在支援jit的平臺上將經常執行的程式碼開啟記錄模型並記錄這些程式碼實際執行每一步的細節,最後被luajit優化以及jit化。
jit失敗情況:並不是所有可以被jit的程式碼都可以正常被jit處理的,以下情況就會造成jit程式碼失敗。
一.可供執行的記憶體空間被耗盡:針對arm平臺有個限制就是跳轉指令只能前後跳轉32m,所以必須至少保證64m的空間被用來儲存jit處理後的機器程式碼。如果這64m中有任何一點記憶體被用作他用就會出現記憶體空間不足而造成jit程式碼失敗。這種情況的優化建議如下:
1.在android工程的Activity入口中就載入luajit,做好記憶體分配,然後將這個luasate傳遞給unity使用。
2.減少lua程式碼,進而減少lua記憶體大小,使其編譯的機器碼可以完整的存放在分配的可執行記憶體中。
3.禁用jit功能。
二.可供使用的暫存器不足:針對arm平臺下,可供使用的暫存器數量會遠遠低於x86平臺,而lua中的lua變數就是儲存在暫存器中的,當local變數越多,或者local變數呼叫層級越深造成local變數生命週期變長而不能及時回收而間接增加local變數個數,都會使暫存器的使用個數變多,這樣就容易出現暫存器不足而造成jit程式碼失敗的情況。這種情況的優化建議如下:
1.減少local變數的使用個數。
2.不要過深層次的呼叫local變數。可以通過do … end來限制local變數的生命週期。
三.呼叫c相關的函式:c#底層也是呼叫c,跟直接呼叫c一樣,這些c相關的程式碼是不能被jit化的。這種情況的優化建議如下:
1.使用luajit中提供的ffi工具來呼叫c相關程式碼,這樣就可以被jit化。
2.使用luajit 2.1.0beta2版本中的trace switch功能,將c程式碼獨立出來,從而將可以jit的程式碼進行jit處理。但是效果有限且不是很明顯。
四.不支援的位元組碼:有非常多bytecode或者內部庫呼叫是無法jit化的,最典型就是for in pairs,以及字串連線符。常見的不能jit處理的對比列表如以下連結,凡是標記2.1以及yes的都儘量少用,最好不用。
http://wiki.luajit.org/NYI
五.jit失敗判定:我們可以使用luajit目錄下的v.lua檔案進行檢視是否jit程式碼失敗,程式碼如下:
local verbo = require(“jit.v”)
verbo.start()
當你看到以下錯誤的時候,說明你遇到了jit失敗
failed to allocate mcode memory,對應錯誤一
NYI: register coalescing too complex,對應錯誤二
NYI: C function,對應錯誤三
NYI: bytecode,對應錯誤四
這在luajit.exe下使用會很正常,但要在unity下用上需要修改v.lua的程式碼,把所有out:write輸出導向到Debug.Log裡頭。
2.儘量使用local function而不是class方式:因為class呼叫function方式都是從metatable中進行表查詢來呼叫的,相對local func = Class.Func的呼叫而言,效能肯定是低了點,但是可讀性卻強了很多。所以使用local function還是class function形式還是視情況而定。對於頻繁呼叫的函式,如Vector3的操作等,建議用local function,這樣效能會提高很多,而針對主要程式碼可以按照class function形式,這樣更加面向物件,可讀性也會強很多。
3.藉助ffi可以提升lua與c/c#之間的互動:ffi是luajit提供的一個神器,主要用來在jit模式下進行高效的luajit與c之間的互動。其原理就是向luajit中指定c函式原型,這樣luajit就可以直接生成機器碼級別的優化程式碼來與c互動,而不需要傳統的lua api來做互動。這樣在與c#互動時,主要留下c到c#的互動消耗了。相關使用方式如下:
首先,我們在c中定義一個方法,用於將c#的函式註冊到c中,以便在c中可以直接呼叫c#的函式,這樣只要luajit可以ffi呼叫c,也就自然可以呼叫c#的函數了
void gse_ffi_register_csharp(int id, void* func)
{
s_reg_funcs[id] = func;
}
這裡,id是一個你自由分配給c#函式的id,lua通過這個id來決定呼叫哪個函式。
然後在c#中將c#函式註冊到c中
[DllImport(LUADLL, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
public static extern void gse_ffi_register_csharp(int funcid, IntPtr func);
public static void gse_ffi_register_v_i1f3(int funcid, f_v_i1f3 func)
{
gse_ffi_register_csharp(funcid, Marshal.GetFunctionPointerForDelegate(func));
}
gse_ffi_register_v_i1f3(1, GObjSetPositionAddTerrainHeight);//將GObjSetPositionAddTerrainHeight註冊為id1的函式
然後lua中使用的時候,這麼呼叫
local ffi = require("ffi")
ffi.cdef[[
int gse_ffi_i_f3(int funcid, float f1, float f2, float f3);
]]
local funcid = 1
ffi.C.gse_ffi_i_f3(funcid, objID, posx, posy, posz)
就可以從lua中利用ffi呼叫c#的函數了
可以類似tolua,將這個註冊流程的程式碼自動生成。
選擇luajit的原因:雖然luajit更新頻率不高,但是相比原生lua而言,luajit在interpreter模式下就可以支援所有平臺,而且提高3到8倍的效能提升,在jit模式下可以提供幾十倍的效能提升,雖然jit模式不是很好控制。所以鑑於出色的效能提升以及豐富的第三方方案來彌補原生lua新特性,所以建議還是繼續使用。
開啟interpreter模式方式:只需要在lua程式碼的第一行加上以下程式碼就行。
if jit then
jit.off();jit.flush()
end