飛書文檔:https://idreamsky.feishu.cn/docs/doccnjZ7tfpP5AFnSWGnlaUDm1h
一、需要注意的數據類型
1. 表table
Lua 實現表的算法頗為巧妙。每個表包含兩部分:數組(array)部分和哈希(hash)部分,數組部分保存的項(entry)以整數為鍵(key),從 1 到某個特定的 n,所有其他的項(包括整數鍵超出范圍的)則保存在哈希部分。
哈希部分使用哈希算法來保存和查找鍵值。它使用的是開放尋址(open address)的表,意味着所有的項都直接存在哈希數組里。鍵值的主索引由哈希函數給出;如果發生沖突(兩個鍵值哈希到相同的位置),這些鍵值就串成一個鏈表,鏈表的每個元素占用數組的一項。
當 Lua 想在表中插入一個新的鍵值而哈希數組已滿時,Lua 會做一次重新哈希(rehash)。重新哈希的第一步是決定新的數組部分和哈希部分的大小。所以 Lua 遍歷所有的項,並加以計數和分類,然后取一個使數組部分用量過半的最大的 2 的指數值,作為數組部分的大小。而哈希部分的大小則是一個容得下剩余項(即那些不適合放在數組部分的項)的最小的 2 的指數值。重新hash的性能消耗還是比較大的。要減少重新hash次數,可以創建大的表格替代多個小的表格或者復用表格。
每次新建一張table,都會產生堆內存,都會導致GC遍歷的時候多一個判斷節點。因此,Lua的GC優化,重點關注table和c的userdata。
在頻繁更新或者使用的代碼部分,不要反復申請table,這會使得虛擬機不斷的去進行內存分配。
1. 追加一個元素到一個array的結尾的三種寫法。其中使用本地計數器的第三種寫法性能最好。
追加一個元素到一個array的結尾的三種寫法。其中使用本地計數器的第三種寫法性能最好。 1. t[#t + 1] = 123 2. talbe.insert(t, 123) 3. local counter = 1 for i = 1, 10000 od t[counter] = i counter = counter + 1 end
配置表優化:見下方。
服務端數據:例如背包中的每個道具數據可以在本地保存一個table,服務端更新時只需要更新對對應道具的table數據而不用每次創建新的表。
其它臨時數據:減少在定時器(幀、秒)或update方法里開辟新的空間(引用全局變量、obj.transform等.操作、創建新的表),可以在循環開始前定義一個local變量做緩存。
2. 字符串string
因為Lua的String是內部復用的,當我們創建字符串的時候,Lua首先會檢查內部是否已經有相同的字符串了,如果有直接返回一個引用,如果沒有才創建。這使得Lua中String的比較和賦值非常地快速,因為只要比較引用是否相等、或者直接賦值引用就可以了。
連接方式:多個字符串連接時使用table.concat代替..的字符串連接。table.concat只會創建一塊buffer,然后在此拼接所有的字符串,實際上是在用table模擬buffer。而..則每次拼接都會產生一串新的字符串,開辟一塊新的buffer。聊天需要更注重這塊內容。
3. 結構體如Vector3
為什么結構體單獨說呢,因為結構體會帶來很嚴重的性能問題,具體原因可以參考:https://www.jianshu.com/p/07dc38e85923以及https://www.gameres.com/700911.html
簡而言之,就是是boxing(裝箱)和unboxing(拆箱)。Vector3(棧)轉為object類型需要boxing(堆內存中),object轉回Vector3需要unboxing,使用后釋放該object引用,這個堆內存被gc檢測到已經沒引用,釋放該堆內存,產生一個gc內存。
tolua\slua 將Vector3等類型實現為純lua代碼,Vector3就是一個{x,y,z}的table,這樣在lua中使用就快了。因為以上結構體,都是table的方式,所以,如果使用頻繁的話,就容易產生大量的堆內存,必要的時候還是用對象池復用,例如坐標系統的點坐標。
使用c#原生的vector,建議在c#端進行封裝,傳值時使用x,y,z進行傳遞,在c#層包裝成vector使用。直接在函數中傳遞三個float,要比傳遞Vector3要更快。
例如void SetPos(GameObject obj, Vector3pos)改為void SetPos(GameObject obj, float x, floaty, float z)
二、lua測優化
參考:https://www.lua.org/gems/sample.pdf
1.首先,我們需要了解類的實現,如下,核心的代碼是setmetatable(cls, {__index = super})這句,訪問 cls 中任何不存在的字段時,都會嘗試到 super 中查找,這里的 super 就相當於父類,而 cls 則相當於是類 super 的子類。
local function __class(classname, super) local superType = type(super) local cls if superType ~= "function" and superType ~= "table" then superType = nil super = nil end if superType == "function" or (super and super.__ctype == 1) then -- inherited from native C++ Object else -- inherited from Lua Object if super then cls = {} setmetatable(cls, {__index = super}) cls.super = super else cls = {ctor = function() end} end cls.__cname = classname cls.__ctype = 2 -- lua cls.__index = cls function cls.ToString(self) return self.__cname end function cls.new(...) local instance = setmetatable({}, cls) instance.class = cls instance:ctor(...) return instance end end return cls end
元表:當訪問表中不存在的字段時,元表中的 __index 元方法會被調用,並返回該方法返回的值,該值可以是一個函數或者表。注意,這邊是訪問不了元表內的屬性的,而是去獲取__index屬性的返回值,如果返回值是函數則調用,返回表則在表內查找字段。測試如下,man無法訪問Person類的isMan字段,但可以訪問__index內的字段name。這就解釋了繼承為什么是setmetatable(cls, {__index = super}),而不是setmetatable(cls, super)。
local Person = { isMan = true, __index = { name = "jadeshu", age = 28, sex = 0, } } --表 local man = {} --表 setmetatable(man,Person) --設置元表 --man的元表是Person --測試 printWJF(man.name) --顯示 jadeshu printWJF(man.isMan,"_",Person.isMan) --顯示 nil_true
2.local變量和_G全局變量、self變量
_G:一張表,保存了lua所用的所有全局函數和全局變量,在默認情況,Lua在全局環境_G中添加了標准庫比如math、函數比如pairs等。如_G.print("你好")=print("你好")。
全局變量不需要聲明,沒被 local 修飾的變量都是全局變量。我們應該減少全局變量的定義,可以把一些全局的屬性放在一個全局表里,在通過這個表訪問。
local:局部變量只在被聲明的那個代碼塊內有效。(代碼塊:指的是一個控制結構內,一個函數體,或者一個chunk(變量被聲明的那個文件或者文本串)),無法通過繼承、元表訪問,類似於c#的private變量。
需要注意的是:使用function聲明的函數為全局函數,在被引用時不會因為聲明的順序而找不到 ,使用local function聲明的函數為局部函數,在引用的時候必須要在聲明的函數后面。
local和_g的優劣見:http://lua-users.org/wiki/OptimisingUsingLocalVariables
1. Local variables are very fast as they reside in virtual machine registers, and are accessed directly by index. Global variables on the other hand, reside in a lua table and as such are accessed by a hash lookup.
所以盡量使用local變量。local變量包括屬性以及方法,一些經常或在循環用到的全局函數,可以申明為local局部變量,這樣可以提升效率。例如表插入操作local TINSERT = table.insert。
self:代表當前表(模塊),可以理解成c#的this,子類可以訪問父類的self屬性,不能訪問local屬性。如下,module作為父類或被require加載出來后,lParam 不能在模塊外部訪問,他們並不在最后return的module表里。constant和constant1做為module表里的內容可以被外部訪問。
self.xxx定義的變量訪問速度比local較慢,因為self查找會走元表,如果多重嵌套,效率肯定是比不上local的,但是self變量可以被外部模塊訪問,一些需要提供給外部的數據比較方便,當然你也可以把local封裝一個Get方法。
-- 文件名為 module.lua -- 定義一個名為 module 的模塊 module = {} local lParam = "這是一個局部變量" -- 定義一個常量 module.constant = "這是一個公共變量" -- 定義一個函數 function module:func1() self.constant1 = "這也是一個公共變量" end local function func2() print("這是一個私有函數!") end return module
3.配置表
緩存:使用時動態加載。
緩存處理一般有:1.常駐內存,加載后不銷毀;2.定時清理,加載后一定時間內未使用則清理,使用則刷新時間;3.一次性,不緩存;4.跟隨場景,只在切換場景時清除配置表;
優化:參考https://blog.uwa4d.com/archives/1490.html。核心點是
1.通過工具將excel表轉為lua文件,通過table的方式訪問表格。
2.提取配置表中大量重復的默認值、表格、數組等作為表的元表,減少重復變量尤其是重復的空表。
3.對配置表中只在客戶端、服務端單項使用的字段進行分離,也就是說只有服務端用到的字段不導出到客戶端的表格。
4.字符串處理,例如說明字段、標題等配置在多語言的表格里,在使用key值索引到對應的多語言項,多語言配置最好一個語言一張表,當前游戲使用哪個語種就加載哪個配置文件。
最終結構類似於:ARENA下的每一條數據的元表設置成默認值,當在數據里找不到指定key,會在元表(也就是默認值defaultValues)里查找默認值。這邊的設置_index實際上相當於設置父類,當前表里查不到對象時,會在_index對象內查找,具體可以看源碼里lua class的實現。
local defaultValues = { robotName = "des_3115", } local ARENA = { [1] = { rank = { 1, 1, }, robotGroupId = 5000, }, [2] = { rank = { 2, 2, }, robotGroupId = 4999, }, [3] = { rank = { 3, 3, }, robotGroupId = 4998, }, [4] = { rank = { 4, 4, }, robotGroupId = 4997, }, [5] = { rank = { 5, 5, }, robotGroupId = 4996, }, [6] = { rank = { 6, 6, }, robotGroupId = 4995, }, [7] = { rank = { 7, 7, }, robotGroupId = 4994, }, } do local base = { __index = defaultValues, --基類,默認值存取 __newindex = function() --禁止寫入新的鍵值 error("Attempt to modify read-only table") end } for k, v in pairs(ARENA) do setmetatable(v, base) end base.__metatable = false --不讓外面獲取到元表,防止被無意修改 end return ARENA
4.不要在for循環中創建表和閉包
local t = {1,2,3,'hi'} for i=1,n do --執行邏輯,但t不更改 ... end
5.建議在場景切換時主動調用一次GC,包括lua、c#的gc方法。
三、與c#的交互優化
1. 交互優化
參考:https://gameinstitute.qq.com/community/detail/125117
gameobj.transform.position = pos調用棧如下:
調用函數:Lua中如果要調用一次C#的函數,至少有幾個步驟:
1、在Lua層面,找到C#這個函數的Wrapper的C指針
2、C#層面,進行參數個數,參數類型的驗證
3、不同類型的參數校驗成本又是不一樣的
Number類型,調用LuaDLL.luaL_checknumber 進行一次驗證即可
String類型,需要先LuaDLL.lua_type 獲取類型,根據不同類型再調用一次LuaDLL的對應tostring接口
Struct類型,如Vector3等,需要調用LuaDLL.tolua_getvec3獲取結構體的值,再new一個Vector3
4、返回值處理
優化建議:
盡量減少不需要的交互,能在lua完成的就在lua完成。
lua端減少長串的點號操作,例如child.parent.tranfrom.localposition,建議在c#封裝SetParentLocalPosition方法。
lua端減少對結構體(如vector)的直接操作,頻繁使用的可以用tolua等封裝的組件,非頻繁的可以在c#額外封裝方法,示例如下。
public static DateTime GetLocalServerTime() { return com.geargames.common.utils.Utils.ServerDateTimeNow().GGToLocalTime(); } public static void SetLocalPositionEx(this Component cmpt, float x, float y, float z) { cmpt.transform.localPosition = new Vector3(x, y, z); }
運行效率測試腳本如下,訪問次數為100000次:
例1:
1、local pos = me.Root.transform.position
2、local pos = me.Root:GetLocalPosition()
3、local x,y,z = me.Root:GetLocalPositionEx()
測試結果(單位秒):
0.22617602348328
0.1167140007019
0.052457094192505
例2:
local y = me.Root.transform.localPosition.y
local y = me.Root:GetLocalPositionY()
測試結果:
0.2229311466217
0.052457094192505
測試代碼:
public static float GetLocalPositionY(this Component cmpt) { return cmpt.transform.localPosition.y; } public static void GetLocalPositionEx(this Component cmpt, out float x, out float y, out float z) { Transform trans = cmpt.transform; x = trans.localPosition.x; y = trans.localPosition.y; z = trans.localPosition.z; } local pos = Vector3(0,0,0) for i=1,100000 do me.Root:SetLocalPosition(pos) --0.058831930160522 --me.Root:SetLocalPositionEx(0,0,0) -- 0.063822984695435 --me.Root:SetLocalPosition(Vector3(0,0,0)) --0.13435101509094 End
lua端減少頻繁獲取unity組件(如GetComponent\Find等方法),頻繁使用的組件建議緩存到本地。建議使用導出工具獲取需要操作的對象,而不是find的方法,避免忘記釋放導致的泄露問題。
減少在循環里通過.獲取c#對象的屬性,如果可以,請緩存它們。
封裝方法注意點:
1. lua和c#之間傳參、返回時,盡可能不要傳遞以下類型:
2. 嚴重類: Vector3/Quaternion等unity值類型,數組
3. 次嚴重類:bool string 各種object
4. 建議傳遞:int float double
5. 頻繁調用的函數,參數的數量要控制,無論是lua的pushint/checkint,還是c到c#的參數傳遞,參數轉換都是最主要的消耗,而且是逐個參數進行的,因此,lua調用c#的性能,除了跟參數類型相關外,也跟參數個數有很大關系。
6. 優先使用static函數導出,減少使用成員方法導出
7. 合理利用out關鍵字返回復雜的返回值
2. 精簡lua導出
網上已經有非常多IL2CPP導致包體積激增的抱怨,而基於lua靜態導出后,由於生成了大量的導出代碼。這個問題又更加嚴重。
鑒於目前ios必須使用IL2CPP發布64bit版本,所以這個問題必須要重視,否則不但你的包體積會激增,binary是要加載到內存的,你的內存也會因為大量可能用不上的lua導出而變得吃緊。
移除你不必要的導出,尤其是unityengine的導出。如果只是為了導出整個類的一兩個函數或者字段,重新寫一個util類來導出這些函數,而不是整個類進行導出。也可以使用[notolua]屬性來標記不導出。例如我們只用到Animation的Play方法,不需要整個導出Animation類,只需要導出對應方法,或封裝一個方法導出。
如果有把握,可以修改自動導出的實現,自動或者手動過濾掉不必要導出的東西。
3. 引用移除
兩端保存的引用及時清除,例如lua持有的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類。
四、內存優化工具
Lua 提供了以下函數collectgarbage ([opt [, arg]])用來控制自動內存管理:
- • collectgarbage("collect"): 做一次完整的垃圾收集循環。通過參數 opt 它提供了一組不同的功能:
- • collectgarbage("count"): 以 K 字節數為單位返回 Lua 使用的總內存數。 這個值有小數部分,所以只需要乘上 1024 就能得到 Lua 使用的准確字節數(除非溢出)。
- • collectgarbage("restart"): 重啟垃圾收集器的自動運行。
- • collectgarbage("setpause"): 將 arg 設為收集器的 間歇率。 返回 間歇率 的前一個值。
- • collectgarbage("setstepmul"): 返回 步進倍率 的前一個值。
- • collectgarbage("step"): 單步運行垃圾收集器。 步長"大小"由 arg 控制。 傳入 0 時,收集器步進(不可分割的)一步。 傳入非 0 值, 收集器收集相當於 Lua 分配這些多(K 字節)內存的工作。 如果收集器結束一個循環將返回 true 。
- • collectgarbage("stop"): 停止垃圾收集器的運行。 在調用重啟前,收集器只會因顯式的調用運行。
如何監測Lua的編程產生內存泄露:
1. 針對會產生泄露的函數,先調用collectgarbage("collect")和collectgarbage("count"),取得最初的內存使用情況。
2. 函數調用后, collectgarbage("collect")進行收集, 並使用collectgarbage("count")再取得當前內存, 最后記錄兩次的使用差。
可以保存函數調用前后的_G到本地文件,然后使用軟件比較前后兩次的_G的內容差,可以獲取到泄漏的具體內容。文件差異對比軟件:https://blog.csdn.net/liuyukuan/article/details/5980591
當然,推薦使用現成的工具lua profile,下載及文檔鏈接:https://github.com/ElPsyCongree/LuaProfiler-For-Unity#zh