Unity下XLua方案的各值類型GC優化深度剖析


轉自:http://gad.qq.com/article/detail/25645

 

前言

Unity下的C#GC Alloc(下面簡稱gc)是個大問題,而嵌入一個動態類型的Lua后,它們之間的交互很容易就產生gc,各種Lua方案也把這作為性能優化的重點。這些優化說穿了其實不復雜。

元凶在這里

先看看這兩個函數

1
2
3
4
5
6
7
8
9
int inc1( int i)
{
     return i + 1;
}
 
object inc2( object o)
{
     return ( int )o + 1;
}

 

window下實測inc1的性能是inc2的20倍!

差距為什么那么大?主要原因在其參數及返回的類型,inc2參數是object類型,意味着一個值類型傳入(比如整數)需要boxing,具體一點就是在堆上申請一塊內存,把類型信息,值拷貝進去,要使用的時候需要unboxing,也就是從剛剛那堆內存拷貝到棧上,等函數執行完畢后,這個堆內存被gc檢測到已經沒引用,釋放該堆內存。

20倍差距是一個參數一個返回的情況,隨着這樣的參數加多,差距更大。而且更糟糕的是:GC比較難控制,Unity的手游項目,GC往往是卡頓的元凶。

目前所有lua方案針對lua和c#間交互的gc優化,或者值類型優化,其實都是在做一件事:避免inc2的情況

C#調用Lua避免inc2

Lua是一門動態類型語言,它的函數可以接受任意類型,任意個數的參數,返回值也是任意類型,任意個數。如果希望以一個通用接口去訪問lua函數,情況會比inc2更糟糕:為了支持任意類型任意個數參數,我們可能得用可變參數;為了支持任意類型多返回值,這個接口可能需要返回一個object數組,而不是一個object。因而我們還多了兩個數組要分配及釋放。函數原型大致如下:

object[]Call(params object[] args)

因為以上原因,大多方案雖然都提供了這種方式(因為方便),但又不推薦使用。有的方案會提供無GC的用法,例如ulua如果要避免gc,得這么來:

1
2
3
4
5
6
var func = lua.GetFunction( "inc" );  
func.BeginPCall();
func.Push(123456);
func.PCall();
int num = ( int )func.CheckNumber();
func.EndPCall();

思路是把lua的棧操作api暴露出來,一個個參數的壓棧,調用完一個個返回值的取。這些壓棧和取返回值的接口都是確定類型的,換句話也就是inc1的接口。

上面只是單參數,單返回值的情況,大多數情況代碼會更繁瑣。

而slua沒有找到相關的方案。

 

xLua的解決辦法的核心思想是:只要你告訴我要用什么參數調用,我幫你優化。

1
2
3
4
[CSharpCallLua]
public delegate int Inc( int i);
Inc func= luaenv.Global.Get( "inc" );
int num =  func(123456);

1、按你所需聲明一個delegate,打上CSharpCallLua標簽;

2、執行生成代碼;

3、用Table的Get接口把inc函數映射到func委托;

4、接下來就可以愉快的使用這個delegate了。

多復雜的參數都是和上面一樣:聲明,獲取,使用。僅比帶gc的Call接口多了一步聲明,使用上和Call接口一樣簡單,甚至處理返回值方面更簡單些,而且還額外帶來強類型檢查的好處。

如果lua函數有多個返回值怎么辦?

多返回值將會映射到C#的返回值以及各輸出參數,從左往右一一映射。

除此之外,xLua還支持一個lua table映射到一個C# interface,對這個interface的屬性訪問會訪問到lua table的對應字段,成員方法調用會調用到lua table里頭的對應函數。同樣的,無gc。

這是如何做到的呢?說起來也不復雜,以lua函數映射到c# delegate為例,xLua會對聲明了CSharpCallLua的delegate生成一段代碼,比如Inc的生成代碼會類似這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
public int SystemInt32( int x)
{
     //...init
     LuaAPI.lua_getref(L, _Reference);
              
     LuaAPI.xlua_pushinteger(L, x);
     int __gen_error = LuaAPI.lua_pcall(L, 1, 1, err_func);
 
     //...error handle
     int __gen_ret = LuaAPI.xlua_tointeger(L, err_func + 1);
     LuaAPI.lua_settop(L, err_func - 1);
     return  __gen_ret;
}

 

Get方法返回的委托將會指向這個方法。從這段代碼來看,和ulua無gc代碼類似,不同的是,別人家得手寫,而且由於xLua少了一層封裝,直接調用Lua的api,應該也更高效些。

 

復雜值類型優化

從C#到lua的復雜對象傳遞說起

lua虛擬機,對於.net就是非托管代碼,要傳遞對象過去,得解決幾個問題:

1、lua使用該對象期間,該對象不能被gc;

2、如果非托管代碼(lua)回調托管代碼(c#),當回傳該對象的引用時,應該正確找到對應的對象;

3、重復傳遞一個對象,在unmanaged code測的引用表示最好是一致的;

問題1和問題2 官方給的方案是pined對象,實測pined一個對象以及釋放的性能大致和Dictionary的Set/Get相當,而問題1和問題2可以優化為數組操作,性能可以比Pined方案高4~5倍:接受一個對象,在一個數組上找到一個空位置放進去,返回數組的下標作為對象引用。通過鏈表組織空位置,空位置查找可以優化成O(1)操作,而通過引用找對象當然也是O(1)。

問題3沒啥好的解決辦法,用Dictionary建立對象到引用的索引。

 

復雜值類型的困境

C#一切都是對象,自然也包括值類型,也能沿用上面的方案,這功能上沒問題,性能卻遭遇了滑鐵盧:

每一次值類型放入對象池(指的是前面一節中提到的為了解決3個問題而做的一套機制)中就會碰到inc2的情況,會boxing成一個新對象,還有入池的一系列操作。有人會問用pined方案會不會沒這問題,其實是一樣的,值類型是在棧上,而pined了之后要從棧轉到堆上,棧轉堆還是會有類似的過程:分配堆內存,拷貝,用完釋放。

這問題比前面那問題影響面更廣,只要C#往lua傳遞一個復雜值類型就會出現,比如普普通通的Vector3四則運算會產生大量的gc。

ulua和slua思路是一樣的,對特定的幾個U3D值類型(Vector2, Vector3,Vector4,Quaternion)做硬編碼優化,以Vector3為例:

1、用lua重新實現了Vector3的所有方法;

2、C#的Vector3傳入lua:是先在lua側建一個luatable,把待傳入Vector3的x,y,z設置為對應字段;設置該table的metatable為1的方法實現;

3、lua回傳Vector3到C#:C#構建一個Vector3后,取出對應table的x,y,z字段賦值到Vector3;

 

xLua的復雜值類型優化

上面的優化存在一些問題:需要增加一種新的值類型十分困難,所以目前為止采用這種方案能支持的值類型手指頭就能數得過來,用戶自定義的struct就更不可能支持了,核心代碼深度耦合這幾個類型也是不合理的。還有個比較嚴重的問題:xLua作者比較抗拒硬編碼這種行為。

讓我們思考一下,ulua和slua那種優化能避免gc的本質是什么?還有簡單值類型從C#傳遞到lua也沒產生gc,原因是什么?

答案就是:值拷貝

ulua和slua的復雜值類型優化,從C#傳遞到lua本質上是把Vector3值拷貝到lua table,避免了入池進而避免了inc2;簡單值類型也是,一個c#的int傳入lua,也是直接把int值拷貝到lua的棧上。

明白了這個思路就開闊很多,xLua設計了一套新的值類型方案,只要一個struct里頭只包含值類型,可嵌套struct,當然,要求被嵌套的struct也只包含值類型,該方法都適用。

原理也不復雜:

1、生成struct的值拷貝代碼,用於把struct里頭各字段拷貝到一塊非托管內存(Pack函數),以及從非托管內存拷貝出各字段的值(UnPack函數);

2、c#傳struct到lua:調用lua的api,申請一塊userdata(對於c#來說是非托管代碼),調用Pack把struct打包進去;

3、lua回傳到給c#:調用UnPack解出struct;

4、struct的方法還是沿用c#原本的實現;

說穿了,就和pb類似,把c#的數據結構序列化到一塊內存以及從內存反序列化回來。

先說這方案的缺點:

缺點源於這個方案調用struct的方法還是調用原來C#的實現。從lua經C語言,再經pinvoke調用到C#,這個適配的成本已經遠遠大於一些簡單方法執行的開銷。當然,xLua只是默認調用C#的實現,也不是必須的,xLua提供了不經過C#,在C就直接讀取更改struct字段的API,比較勤快的童鞋利用這API,可以嘗試把需要高性能的地方用Lua實現,這就避免了lua和C#間的適配成本。

PS一下:網上很流行的lua方案性能用例,用Vector3.Normalize來測試lua調用c#靜態函數的性能,甚至Unity官方發的測評都用這個用例。由前面的分析可以知道,這是不對的,這些被測方案的Vector3.Normalize都僅在lua里頭跑,壓根沒測試到“lua調用c#靜態函數”。

這方案優點:

1、支持的struct類型寬泛的多,用戶要做的事情也很簡單,聲明一下要生成代碼即可(GCOptimize),之所以要聲明,主要是避免生成代碼過多;

2、相比table方案更省內存,只是struct的大小加上一個頭部,而64位下空table就80字節+,實際測試Vector3的userdata方案的內存占用是table方案三分之一;

 

其它值類型GC優化

下面大多數優化都只在xLua有效,可以在其05_NoGc示例看到用法,生成代碼后運行在profiler看你效果。

1、枚舉類型傳遞無GC;

2、decimal不丟失精度而且無GC;

3、所有無GC的類型,它的數組訪問沒有GC,這個貌似大多數方案都做到;

4、能被GCOptimize優化的struct,在Lua可以直接傳一個對應結構的table,無GC;

5、LuaTable提供一系列泛化Get/Set接口,可以傳遞值類型而無GC;

6、一個interface加入到CSharpCallLua后,可以用table來實現這個interface,通過這interface訪問table無GC;

這些優化和前面介紹的兩大思路一脈相承,可以通過源代碼看其實現,這就不分析了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM