lua 性能優化


飛書文檔: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,包括luac#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. luac#之間傳參、返回時,盡可能不要傳遞以下類型:

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


免責聲明!

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



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