用好lua+unity,讓性能飛起來——lua與c#交互篇


前言

在看了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互轉)。
可以參考 https://msdn.microsoft.com/zh-cn/library/ms998551.aspx,這里有更詳細的關於c和c#交互的性能優化指引。
而嚴重類,基本上是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;
    }

}

 


免責聲明!

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



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