在看了uwa以前發佈的《Unity項目常見Lua解決方案性能比較》,決定動手寫一篇關於lua+unity方案的性能優化文。
整合lua是目前最強大的unity熱更新方案,畢竟這是惟一能夠支持ios熱更新的辦法。然而做爲一個重度ulua用戶,咱們踩過了不少的坑纔將ulua上升到一個能夠在項目中大規模使用的狀態。事實上即便到如今lua+unity的方案仍不能輕易的說能夠肆意使用,要用好,你須要知道不少。
所以,這篇文章是從一堆簡單的優化建議裏頭,逐步挖掘出背後的緣由。只有理解了緣由,才能很清楚本身作的優化,究竟是爲了什麼,有多大的效果。
從最先的lua純反射調用c#,以及雲風團隊嘗試的純c#實現的lua虛擬機,一直髮展到如今的各類luajit+c#靜態lua導出方案,lua+unity纔算達到了性能上實用的級別。
但即便這樣,實際使用中咱們會發現,比起cocos2dx時代luajit的發揚光大,如今lua+unity的性能依然存在着至關的瓶頸。僅從《性能比較》的test1就能夠看到,iphone4s下二十萬次position賦值就已經須要3000ms,若是是coc這樣類型的遊戲,不處理其餘邏輯,一幀僅僅上千次位置賦值(好比數百的單位、特效和血條)就須要15ms,這顯然有些偏高。
是什麼致使lua+unity的性能並未達到極致,要如何才能更好的使用?咱們會一些例子開始,逐步挖掘背後的細節。
因爲咱們項目主要使用的是ulua(集成了topameng的cstolua,可是因爲持續的性能改進,後面已經作過大量的修改),本文的大部分結論都是基於ulua+cstolua的測試得出來的,slua都是基於其源碼來分析(根據咱們分析的狀況來看,二者原理上基本一致,僅在實現細節上有一些區別),但沒有作過深刻測試,若有問題的話歡迎交流。
既然是lua+unity,那性能好很差,基本上要看兩大點:
lua跟c#交互時的性能如何
純lua代碼自己的性能如何
由於這兩部分都各有本身須要深刻探討的地方,因此咱們會分爲多篇去探討整個lua+unity到底如何進行優化。
lua與c#交互篇
1.從致命的gameobj.transform.position = pos開始提及
像gameobj.transform.position = pos這樣的寫法,在unity中是再常見不過的事情
可是在ulua中,大量使用這種寫法是很是糟糕的。爲何呢?
由於短短一行代碼,卻發生了很是很是多的事情,爲了更直觀一點,咱們把這行代碼調用過的關鍵luaapi以及ulua相關的關鍵步驟列出來(以ulua+cstolua導出爲準,gameobj是GameObject類型,pos是Vector3):
第一步:
GameObjectWrap.get_transform lua想從gameobj拿到transform,對應gameobj.transform
LuaDLL.luanet_rawnetobj 把lua中的gameobj變成c#能夠辨認的id
ObjectTranslator.TryGetValue 用這個id,從ObjectTranslator中獲取c#的gameobject對象
gameobject.transform
準備這麼多,這裏終於真正執行c#獲取gameobject.transform了
ObjectTranslator.AddObject 給transform分配一個id,這個id會在lua中用來表明這個transform,transform要保存到ObjectTranslator供將來查找
LuaDLL.luanet_newudata 在lua分配一個userdata,把id存進去,用來表示即將返回給lua的transform
LuaDLL.lua_setmetatable 給這個userdata附上metatable,讓你能夠transform.position這樣使用它
LuaDLL.lua_pushvalue 返回transform,後面作些收尾
LuaDLL.lua_rawseti
LuaDLL.lua_remove
第二步:
TransformWrap.set_position lua想把pos設置到transform.position
LuaDLL.luanet_rawnetobj 把lua中的transform變成c#能夠辨認的id
ObjectTranslator.TryGetValue 用這個id,從ObjectTranslator中獲取c#的transform對象
LuaDLL.tolua_getfloat3 從lua中拿到Vector3的3個float值返回給c#
lua_getfield + lua_tonumber 3次 拿xyz的值,退棧
lua_pop
transform.position = new Vector3(x,y,z)
準備了這麼多,終於執行transform.position = pos賦值了
就這麼一行代碼,居然作了這麼一大堆的事情!若是是c++,a.b.c = x這樣通過優化後無非就是拿地址而後內存賦值的事。可是在這裏,頻繁的取值、入棧、c#到lua的類型轉換,每一步都是滿滿的cpu時間,還不考慮中間產生了各類內存分配和後面的GC!
下面咱們會逐步說明,其中有一些東西實際上是沒必要要的,能夠省略的。咱們能夠最終把他優化成:
lua_isnumber + lua_tonumber 4次,所有完成
2.在lua中引用c#的object,代價昂貴
從上面的例子能夠看到,僅僅想從gameobj拿到一個transform,就已經有很昂貴的代價
c#的object,不能做爲指針直接供c操做(其實能夠經過GCHandle進行pinning來作到,不過性能如何未測試,並且被pinning的對象沒法用gc管理),所以主流的lua+unity都是用一個id表示c#的對象,在c#中經過dictionary來對應id和object。同時由於有了這個dictionary的引用,也保證了c#的object在lua有引用的狀況下不會被垃圾回收掉。
所以,每次參數中帶有object,要從lua中的id表示轉換回c#的object,就要作一次dictionary查找;每次調用一個object的成員方法,也要先找到這個object,也就要作dictionary查找。
若是以前這個對象在lua中有用過並且沒被gc,那還就是查下dictionary的事情。但若是發現是一個新的在lua中沒用過的對象,那就是上面例子中那一大串的準備工做了。
若是你返回的對象只是臨時在lua中用一下,狀況更糟糕!剛分配的userdata和dictionary索引可能會由於lua的引用被gc而刪除掉,而後下次你用到這個對象又得再次作各類準備工做,致使反覆的分配和gc,性能不好。
例子中的gameobj.transform就是一個巨大的陷阱,由於.transform只是臨時返回一下,可是你後面根本沒引用,又會很快被lua釋放掉,致使你後面每次.transform一次,均可能意味着一次分配和gc。
3.在lua和c#間傳遞unity獨有的值類型(Vector3/Quaternion等)更加昂貴
既然前面說了lua調用c#對象緩慢,若是每次vector3.x都要通過c#,那性能基本上就處於崩潰了,因此主流的方案都將Vector3等類型實現爲純lua代碼,Vector3就是一個{x,y,z}的table,這樣在lua中使用就快了。
可是這樣作以後,c#和lua中對Vector3的表示就徹底是兩個東西了,因此傳參就涉及到lua類型和c#類型的轉換,例如c#將Vector3傳給lua,整個流程以下:
1.c#中拿到Vector3的x,y,z三個值
2.push這3個float給lua棧
3.而後構造一個表,將表的x,y,z賦值
4.將這個表push到返回值裏
一個簡單的傳參就要完成3次push參數、表內存分配、3次表插入,性能可想而知。
那麼如何優化呢?咱們的測試代表,直接在函數中傳遞三個float,要比傳遞Vector3要更快。
例如void SetPos(GameObject obj, Vector3 pos)改成void SetPos(GameObject obj, float x, float y, float z)
具體效果能夠看後面的測試數據,提高十分明顯。
4.lua和c#之間傳參、返回時,儘量不要傳遞如下類型:
嚴重類: Vector3/Quaternion等unity值類型,數組
次嚴重類:bool string 各類object
建議傳遞:int float double
雖然是lua和c#的傳參,可是從傳參這個角度講,lua和c#中間其實還夾着一層c(畢竟lua自己也是c實現的),lua、c、c#因爲在不少數據類型的表示以及內存分配策略都不一樣,所以這些數據在三者間傳遞,每每須要進行轉換(術語parameter mashalling),這個轉換消耗根據不一樣的類型會有很大的不一樣。
先說次嚴重類中的bool string類型,涉及到c和c#的交互性能消耗,根據微軟官方文檔,在數據類型的處理上,c#定義了Blittable Types和Non-Blittable Types,其中bool和string屬於Non-Blittable Types,意思是他們在c和c#中的內存表示不同,意味着從c傳遞到c#時須要進行類型轉換,下降性能,而string還要考慮內存分配(將string的內存複製到託管堆,以及utf8和utf16互轉)。
而嚴重類,基本上是ulua等方案在嘗試lua對象與c#對象對應時的瓶頸所致。
Vector3等值類型的消耗,前面已經有所說起。
而數組則更甚,由於lua中的數組只能以table表示,這和c#下徹底是兩碼事,沒有直接的對應關係,所以從c#的數組轉換爲lua table只能逐個複製,若是涉及object/string等,更是要逐個轉換。
5.頻繁調用的函數,參數的數量要控制
不管是lua的pushint/checkint,仍是c到c#的參數傳遞,參數轉換都是最主要的消耗,並且是逐個參數進行的,所以,lua調用c#的性能,除了跟參數類型相關外,也跟參數個數有很大關係。通常而言,頻繁調用的函數不要超過4個參數,而動輒十幾個參數的函數若是頻繁調用,你會看到很明顯的性能降低,手機上可能一幀調用數百次就能夠看到10ms級別的時間。
6.優先使用static函數導出,減小使用成員方法導出
前面提到,一個object要訪問成員方法或者成員變量,都須要查找lua userdata和c#對象的引用,或者查找metatable,耗時甚多。直接導出static函數,能夠減小這樣的消耗。
像obj.transform.position = pos。
咱們建議的方法是,寫成靜態導出函數,相似
class LuaUtil{
static void SetPos(GameObject obj, float x, float y, float z){obj.transform.position = new Vector3(x, y, z); }
}
而後在lua中LuaUtil.SetPos(obj, pos.x, pos.y, pos.z),這樣的性能會好很是多,由於省掉了transform的頻繁返回,並且還避免了transform常常臨時返回引發lua的gc。
7.注意lua拿着c#對象的引用時會形成c#對象沒法釋放,這是內存泄漏常見的原由
前面說到,c# object返回給lua,是經過dictionary將lua的userdata和c# object關聯起來,只要lua中的userdata沒回收,c# object也就會被這個dictionary拿着引用,致使沒法回收。
最多見的就是gameobject和component,若是lua裏頭引用了他們,即便你進行了Destroy,也會發現他們還殘留在mono堆裏。
不過,由於這個dictionary是lua跟c#的惟一關聯,因此要發現這個問題也並不難,遍歷一下這個dictionary就很容易發現。ulua下這個dictionary在ObjectTranslator類、slua則在ObjectCache類
8.考慮在lua中只使用本身管理的id,而不直接引用c#的object
想避免lua引用c# object帶來的各類性能問題的其中一個方法就是本身分配id去索引object,同時相關c#導出函數再也不傳遞object作參數,而是傳遞int。
這帶來幾個好處:
1.函數調用的性能更好;
2.明確地管理這些object的生命週期,避免讓ulua自動管理這些對象的引用,若是在lua中錯誤地引用了這些對象會致使對象沒法釋放,從而內存泄露
3.c#object返回到lua中,若是lua沒有引用,又會很容易立刻gc,而且刪除ObjectTranslator對object的引用。自行管理這個引用關係,就不會頻繁發生這樣的gc行爲和分配行爲。
例如,上面的LuaUtil.SetPos(GameObject obj, float x, float y, float z)能夠進一步優化爲LuaUtil.SetPos(int objID, float x, float y, float z)。而後咱們在本身的代碼裏頭記錄objID跟GameObject的對應關係,若是能夠,用數組來記錄而不是dictionary,則會有更快的查找效率。如此下來能夠進一步省掉lua調用c#的時間,而且對象的管理也會更高效。
9.合理利用out關鍵字返回複雜的返回值
在c#向lua返回各類類型的東西跟傳參相似,也是有各類消耗的。
好比
Vector3 GetPos(GameObject obj)
能夠寫成
void GetPos(GameObject obj, out float x, out float y, out float z)
表面上參數個數增多了,可是根據生成出來的導出代碼(咱們以ulua爲準),會從:
LuaDLL.tolua_getfloat3(內含get_field + tonumber 3次)
變成
isnumber + tonumber 3次
get_field本質上是表查找,確定比isnumber訪問棧更慢,所以這樣作會有更好的性能。
實測
好了,說了這麼多,不拿點數據來看仍是太晦澀
爲了更真實地看到純語言自己的消耗,咱們直接沒有使用例子中的gameobj.transform.position,由於這裏頭有一部分時間是浪費在unity內部的。
咱們重寫了一個簡化版的GameObject2和Transform2。
class Transform2{
public Vector3 position = new Vector3();
}
class GameObject2{
public Transform2 transform = new Transform2();
}
而後咱們用幾個不一樣的調用方式來設置transform的position
方式1:gameobject.transform.position = Vector3.New(1,2,3)
方式2:gameobject:SetPos(Vector3.New(1,2,3))
方式3:gameobject:SetPos2(1,2,3)
方式4:GOUtil.SetPos(gameobject,
Vector3.New(1,2,3))
方式5:GOUtil.SetPos2(gameobjectid, Vector3.New(1,2,3))
方式6:GOUtil.SetPos3(gameobjectid, 1,2,3)
分別進行1000000次,結果以下(測試環境是windows版本,cpu是i7-4770,luajit的jit模式關閉,手機上會由於luajit架構、il2cpp等因素干擾有所不一樣,但這點咱們會在下一篇進一步闡述):
方式1:903ms
方式2:539ms
方式3:343ms
方式4:559ms
方式5:470ms
方式6:304ms
能夠看到,每一步優化,都是提高明顯的,尤爲是移除.transform獲取以及Vector3轉換提高更是巨大,咱們僅僅只是改變了對外導出的方式,並不須要付出很高成本,就已經能夠
節省66%的時間。
實際上能不能再進一步呢?還能!在方式6的基礎上,咱們能夠再作到只有200ms!
這裏賣個關子,下一篇luajit集成中咱們進一步講解。通常來講,咱們推薦作到方式6的水平已經足夠。
這只是一個最簡單的案例,有不少各類各樣的經常使用導出(例如GetComponentsInChildren這種性能大坑,或者一個函數傳遞十幾個參數的狀況)都須要你們根據本身使用的狀況來進行優化,有了咱們提供的lua集成方案背後的性能原理分析,應該就很容易去考慮怎麼作了。
下一篇將會寫lua+unity性能優化的第二部分,luajit集成的性能坑
相比起第一部分這種看導出代碼就能大概知道性能消耗的問題,luajit集成的問題要複雜晦澀得多。
附測試用例的c#代碼:
public class Transform2
{
public Vector3 position = new Vector3();
}
public class GameObject2
{
public Transform2 transform = new Transform2();
public void SetPos(Vector3 pos)
{
transform.position = pos;
}
public void SetPos2(float x, float y, float z)
{
transform.position.x = x;
transform.position.y = y;
transform.position.z = z;
}
}
public class GOUtil
{
private static List<GameObject2> mObjs = new List<GameObject2>();
public static GameObject2 GetByID(int id)
{
if(mObjs.Count == 0)
{
for (int i = 0; i < 1000; i++ )
{
mObjs.Add(new GameObject2());
}
}
return mObjs[id];
}
public static void SetPos(GameObject2 go, Vector3 pos)
{
go.transform.position = pos;
}
public static void SetPos2(int id, Vector3 pos)
{
mObjs[id].transform.position = pos;
}
public static void SetPos3(int id, float x, float y ,float z)
{
var t = mObjs[id].transform;
t.position.x = x;
t.position.y = y;
t.position.z = z;
}
}