lua和cs交互優化


整個思路的核心就是:

 

1、通過Lua_topointer,直接獲取Lua table的內存指針。
2、由於Lua/LuaJIT的table內存結構是可以確認的,我們可以對照其C代碼在C#中聲明結構體,這樣就可以通過table指針拿到array的指針以及array的長度。
3、但是,這里有一個難點,就是要處理Lua/LuaJIT的差異,以及在不同編譯選項下產生出來的32位、64位的差異。所以可以看到我們是分LuaAdapter.cs和LuaJitAdapter.cs兩套實現,並且各自提供了32/64位的結構體聲明。
4、不管是Lua還是LuaJIT,array數組存儲的不是int或者double,而是一個叫TValue的聯合體,TValue除了存儲數值本身,還存儲了類型信息。我們在讀寫的時候,需要先判斷類型信息,不然就會無法獲得正確的結果。
5、在了解這些信息之后,整個過程就是:拿到table指針,用對應平台的結構體指針獲得array指針,再通過數組index拿到array中正確位置的TValue,最后根據TValue的類型信息獲得/寫入int或者double。

文章最后提供了實現的下載鏈接,此處演示如何使用這個庫。

Lua端:

C#端:

 

 

具體使用流程:

  • 參考附錄中的方法將代碼整合到游戲中。
  • local luaTable = LuaCSharpArr.New(123)會產生一個長123的Lua數組,長度僅僅是預分配,可以正常擴充,這個數組跟正常的數組是基本一樣的,僅僅在里頭內嵌了一個供C#使用的訪問器LuaArrAccess類。
  • 在Lua端可以直接像普通數組一樣訪問,例如LuaTable[12] = 34.56。
  • local arrAccess = luaTable:GetCSharpAccess(),通過這個方式獲得C#訪問器
  • 自己實現一個C#函數並導出到Lua,用這個函數將C#訪問器從Lua傳遞到C#。
  • 在C#中拿到這個訪問器,然后可以使用arrAccess.SetInt(12, 34)、arrAccess.GetDouble(12)這樣的方式去讀寫數據。    

如何應用

有了這個跨語言共享數據的方案,我們就可以大膽將主要的數據放在Lua層,而不用再為跨語言性能而過度犧牲熱更代碼的覆蓋率。具體怎么設計代碼的架構來利用這個數據共享方式呢?

以《赤潮》這樣的產品為例:

  • 同屏角色200+,必須高度考慮性能。
  • 我們使用了幀同步,一旦線上發現不同步的bug是致命的,必須支持熱更新修復。
  • 每月我們會推出新兵種,每隔幾個月會推出新玩法地圖和節日地圖,無論兵種邏輯還是地圖規則,都需要一定的熱更新能力,減少發布完整包更新的必要性。

在以往的方案里,由於性能和熱更新兩者高度矛盾,要同時做到以上的點,十分困難。 而現在,借助跨語言數據共享,我們可以這樣實現:

  • 常用的邏輯數據(比如角色屬性/角色狀態/buff信息等等),用數組存儲放在Lua,並通過本文源碼中的LuaArrAccess共享到C#內。
  • 主體戰斗邏輯使用C#實現,保證基本性能。
  • C#邏輯直接通過LuaArrAccess讀寫Lua中的數據,而不是自己復制一份。這樣,無論Lua還是C#,都可以自由訪問這些共享的信息。
  • Lua申請一個巨大的數組,共享到C#中,C#可以通過這個數組回傳事件消息,Lua可以響應來自C#的事件,性能比使用C#導出delegate更優。
  • Lua實現網絡同步邏輯,以便支持隨時的熱更同步邏輯,以及熱更網絡協議。
  • Lua處理策划數據表,以便支持隨時熱更策划數據結構變更。
  • Lua處理整個玩法大規則,以便我們在推出新玩法的時候,可以做到熱更新。一般而言,玩法大規則的運算量比較小,並非性能瓶頸,但需要更戰斗邏輯有大量數據交互。
  • Lua作為戰斗邏輯的大入口,有權限管理所有角色/地圖/組件的開關以及生命周期,為hotfix提供最大的便利。
  • 對一些可以模板化的代碼,比如技能邏輯/AI節點等,支持Lua/C#兩種實現方式,以便做到新特性可以直接熱更。

這一切之所以可行,得益於我們可以低消耗跨語言傳遞數據,否則,我們放在Lua的功能(網絡同步/數據表/玩法規則/聲明周期管理/技能AI模板節點),會因為需要頻繁跟C#交互,產生巨大的性能瓶頸。 可見,高效地在兩個語言之間共享數據,是非常重要的。


此外,如果你的項目本身是用Lua做的,要遷移到C#,你也可以利用這個共享機制很輕松地將局部代碼轉移過去。

 

性能對比

由於在Lua和C#間傳遞數據的方法很多,我們對比一下通過傳統C#版Lua導出傳參,以及在C導出API給Lua直接讀寫C內存的性能,進行對比。

環境:Windows + i7 + Unity 2018.3.11f1,Lua使用5.3.5,LuaJIT使用2.1.0beta3(啟用interpreter模式)

1、可以看到,新方案(操作1/2和操作3/4)比起傳統直接Lua調用C#傳遞數據(操作6),性能優化非常大。新方案在兩個語言都做到近乎原生讀寫的性能,讀寫成本基本不再是瓶頸。因此,將代碼按需求分布在Lua和C#之間,將成為可能。

2、其中,C#端的操作3/4性能會較慢,主要原因在於兩方面: 

  • Lua和LuaJIT都區分int和double存儲,C#操作需要進行判斷。其中LuaJIT的判斷較為復雜,耗時也會更多。
  • 為了提高易用性和安全性,本文的源碼增加了一些保護,這些保護並非必須。如果希望追求更高的效率,可以參考附錄自行調整。

3、而Lua原生讀寫數組(操作1)的效率也比通過C API訪問共享內存(操作5)要高一些,提升效率大概是2~3倍的水平。而C#端我們沒有對比,因為我們的實現本質就是訪問共享內存的方式,所以性能本質上是一樣的,完全看共享內存存儲方式的具體實現(比如是否像Lua一樣需要判斷數據類型)。另外考慮到我們訪問不需要編譯C代碼,所以這個方案也是非常有優勢的。

4、另外,由於數據是共享的,就沒有必要在Lua和C#之間分配兩套內存空間存數據,也就提供了節省內存的可能性。

5、事實上,了解Lua底層的朋友會知道,Lua數組(采用1~n連續整數鍵值的表)比Lua hashtable(即有帶命名字段的表)訪問起來要快,且節省內存(TValue的內存占用大概是Node+Key/Value的四分之一)。所以即使使用面向對象的方式開發,從性能最優的角度,我們也鼓勵用Lua數組的方式來替代Lua hashtable的方式,如下代碼所示: 

 

附錄:使用細節說明及源碼下載

1. 如何整合插件

a) 由於代碼使用了unsafe code,所以需要在Unity的player settings勾選Allow unsafe code。
b) 下載本文附件中的代碼,將LuaAdapter.cs和LuaJitAdapter.cs拷貝到工程的任意目錄。
c) 代碼默認按照xLua的標准開發,但如果你使用的不是xLua也很容易集成。

  • 將xLua相關的API替換為對應的API;
  • 向Lua導出LuaArrAccessAPI/LuaArrAccess/LuaTablePin64/LuaJitTablePin共4個類;
  • 由於xLua導出的C#類在Lua中都是按CS.XXX的命名調用,所以需要將LuaCSharpArr.lua.txt內的CS.LuaTableCSharpAccess替換為正確的命名,比如uLua為LuaTableCSharpAccess。

d) 在Lua初始化后,調用LuaTableCSharpAccess.RegisterPinFunc(L);注冊函數,其中參數L是lua state的IntPtr。
e) LuaCSharpArr.lua.txt中包含了LuaCSharpArr的整個定義聲明,可以直接require使用,如果你的Lua代碼require機制不同,將代碼復制到你的Lua可以訪問到的地方即可。
f) 參考示例代碼LuaTestScript.lua.txt以及LuaTestBehaviour.cs使用Lua端的LuaCSharpArr和C#端的LuaArrAccess。
g) 如果想運行測試用例,在空場景中建立一個GameObject,將LuaTestBehaviour拖進去,並將里頭的LuaScript字段附上LuaTestScript.lua.txt即可。

2. 理解Lua array與hashtable

a) 你需要知道,Lua table實際內部是包含一個數組(array)和hashtable來共同存儲所有數據的。其中array用於存儲鍵值為1~n的數據,且1~n必須連續。而其他的數據,會被存放在hashtable內。
b) 必須要注意,Lua數組要求key必須是連續的整數(1~n),如果中間有空洞,那么可能出現的情況是后面的數據會被放到hashtable存儲,也就無法在LuaArrAccess讀取,所以我們提供了預分配機制,防止自己插入的時候出現失誤。

3. 安全讀寫與訪問

a) 要大規模在工程中使用,那么代碼的安全性就很重要,將lua底層數據結構暴露這個事情,本身會破壞Lua的安全性,錯誤的操作可能會導致嚴重的內存錯誤。因此我們提供的實現做了一些機制避免出現問題。

  • LuaArrAccess(C#訪問器)會正確地響應LuaCSharpArr(Lua端數組)被GC的情況。當LuaCSharpArr被GC時會觸發LuaArrAccess.OnGC(),將C#對LuaCSharpArr的引用指針置空。此時LuaArrAccess處於InValid狀態,讀取數值會返回0,寫入數值會被忽略。
  • Lua分配過的內存是保證地址的可持續性的,也就是你用指針引用的數據不會突然間被轉移到其他的內存位置。
  • 前文提到LuaCSharpArr.New會進行預分配,防止數組空洞。
  • LuaArrAccess會檢查數組越界。

b) 使用注意

  • 你可以在Lua中引用LuaCSharpArr,但是不要在Lua直接強引用LuaCSharpArr:GetAccess返回的LuaArrAccess。強引用LuaArrAccess會導致C#端不能正確響應Lua array被GC的情況。
  • Lua array的擴展只能發生在Lua端。也就是如果Lua array長128,你不能在C#端通過LuaArrAccess設置第129項來擴展數組長度。
  • LuaArrAccess:GetArrayCapacity()返回的長度是Lua底層預分配的長度,並不是你在Lua中用#運算符獲取的數組長度。一般GetArrayCapacity返回的值是2的n次方,比#返回的值更大。在這個范圍內讀寫是內存安全的。
  • 你可以通過Lua的#運算符獲取數組長度,確認數組是否有空洞。如果有空洞,則#返回的長度只會等於空洞前面的長度。
  • LuaArrAccess使用與Lua一致的index,也就是從1開始,而不是從0開始。但注意,LuaJIT允許從0開始,這個也是LuaArrAccess支持的。

4. 進一步的性能提升

a) LuaArrAccess的代碼為了易用性和安全性,犧牲了相當程度的性能,讀者如果對性能有更高要求並且有意願修改源碼的話,可以嘗試以下方法提高性能。

  • 跳過Index檢查以及null指針檢查:如果代碼能夠保證數組非固定長度;
  • 使用GetIntFast和GetDoubleFast函數:該系列函數不檢查index范圍,不檢查double和int類型,性能極致高效,但是讀者使用需要相當注意,尤其是int和double的處理要十分小心;
  • 使用C語言重新實現LuaArrAccess:使用C可以獲得比C#更好的性能,但是需要讀者去修改和編譯Lua/LuaJIT的C代碼,限於篇幅關系,這里不提供詳細說明,讀者可以參考LuaArrAccess的代碼直接翻譯到C代碼。

5. 其他問題

a) 目前提供的實現,只支持數組存儲int/double,不支持其他類型(bool/string等)。由於Lua/LuaJIT在默認編譯方式下使用double存儲浮點數,所以不提供float相關的接口。
b) 代碼只支持讀寫數組,無法讀寫hashtable。這里也額外提一點,Lua hashtable雖然使用便利,但是在讀寫效率以及內存占用上,都比lua array要差不少。所以在我們的項目實踐中,會有大量的Lua class使用array來存儲字段。
c) 事實上這個方法可以舉一反三,推廣到用於直接在C#綁定訪問lua中的某個表的一個字段,或者訪問完整的key-value table,不過由於table訪問的復雜性,實現起來會相對復雜一些,不在本文討論的范圍內,讀者可以進一步探索。
d) 本文提供的代碼並非文中提到的《赤潮》所使用的版本,因為《赤潮》使用的代碼基於ulua+luajit2.1.0beta2,也未實現文中所提到的安全訪問特性,且需要修改LuaJIT源碼,所以代碼會有大量不同。
e) 本文的源碼已通過以下平台測試(xLua 2.1.14 + unity2018.3.5)。

  • Windows x86/x64,Lua+LuaJIT
  • Android il2cpp armv7/arm64,LuaJIT
  • Ios il2cpp arm64,LuaJIT

f) 另由於Lua/LuaJIT、xLua/sLua/uLua多版本差異以及Mono/il2cpp/跨平台帶來的復雜性,本文代碼無法確保完全覆蓋所有版本和所有平台的組合情況,如果使用中遇到問題,歡迎在下方留言反饋。


免責聲明!

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



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