宇宙无限 发表于 2021-1-13 08:57

用Unity+Lua开发游戏,有什么好的办法进行性能检测?

用Unity+Lua开发游戏,有什么好的办法进行性能检测?

哈哈SE7 发表于 2021-1-13 09:07

在Unity中使用Lua存在的问题:
主要有三个:
1、进行优化的人对lua、C#、C++整个调用流程结构并不是很清晰。
2、 Lua、Mono双GC系统、以及Mono对象、Lua对象、Unity对象三者的释放流程细节难以把控容易造成资源没有及时释放。
3、没有什么好的办法查看lua这边具体的函数耗时以及GC消耗,导致程序员知道自己的函数内存或者时间消耗不是太理想,却又不知道怎么去优化。


1、进行优化的人对lua、C#、C++整个调用流程结构并不是很清晰。
C#跟Lua脚本相互调用来调用去的,大大限制了优化程序员以及系统功能程序员对整体的代码框架的把控,我们只知道调用API,并不知道两个脚本交互的时候细节上的损耗到底有多大。
例如下面几个很普通的调用
Transform child = transform.find("children1");
transform.position = new Vector3(0,1,0)
transfrom.gameObject.name = "1234"; 在C#那边几乎没有性能问题,但是在lua这边因为字符串传递的消耗,以及lua这边会把Vector3当成table或者userdata处理,很容易就会因为开发者的疏忽从而导致一些意想不到的瓶颈问题。
C#的tostring函数
public static string lua_tostring(IntPtr L, int index)
{
    IntPtr strlen;

    IntPtr str = lua_tolstring(L, index, out strlen);
    if (str != IntPtr.Zero)
        {
      // 这里new了一个字符串
      string ret = Marshal.PtrToStringAnsi(str, strlen.ToInt32());
      if (ret == null)
      {
            int len = strlen.ToInt32();
            // 这里new 了一个byte数组
            byte[] buffer = new byte;
            Marshal.Copy(str, buffer, 0, len);
            // 这里new 了一个字符串
            return Encoding.UTF8.GetString(buffer);
      }
      return ret;
    }
    else
        {
      return null;
        }
} 所以,一定要避免字符串频繁传递,尤其是UI打开的时候获取一些控件,千万别用字符串来。应该在C#那边组织好(比如在prefab中维护一个public List<UnityEngine.Object>,然后直接把对应的对象拽进去),然后通过LuaTable注册给UI操作table。
XLua的 push_struct方法
LUA_API void *xlua_pushstruct(lua_State *L, unsigned int size, int meta_ref) {
    // 在Lua这边new了一个 userdata
    CSharpStruct *css = (CSharpStruct *)lua_newuserdata(L, size + sizeof(int) + sizeof(unsigned int));
    css->fake_id = -1;
    css->len = size;
    lua_rawgeti(L, LUA_REGISTRYINDEX, meta_ref);
    lua_setmetatable(L, -2);
    return css;
}一定要避免在Lua这边使用Vector3计算位移,C#那边是个栈对象,Lua这边是个堆对象,会导致Lua的频繁GC,替换做法可以通过3个number进行传递,然后在c那边封装好数值计算函数
transform.gameObject 在C#那边仅仅只是个地址偏移效率估计就是一次加法和取地址运算,但是在Lua这边却有着非常严重的虚拟机交互过程。所以要尽量的减少__index元运算,多用缓存把一些C#对象保存起来,比如UIManager.GetInstance()这种在C#那边效率影响不大,Lua这边就最好搞个全局变量保存一下。
2、 Lua、Mono双GC系统、以及Mono对象、Lua对象、Unity对象三者的释放流程,细节难以把控,容易造成资源没有及时释放。
Lua将对象传递到C#这边之后,C#这边是个ref,内存占用很小,但是Lua那边可能就是一个UI界面的table或者复杂的函数调用,Lua那边的内存会非常大,并且Lua的一个UI类还引用了不少C#那边的UI控件。也就是说,如果销毁了一个UI界面,在C#这边不立刻把对应的table以及注册到C#的回调函数注销掉的话,整个系统内存将陷入一个Lua与C#相互持有的死锁状态。
正常的UI打开如下:
注:实线代表持有,虚线代表ref引用
首先调用了C#的Destory方法以及确保Lua虚拟机这边没啥对 ui table的引用
static void DisposeUI(string uiName)
{
    // Destory Object 将ui的LuaTable的置空掉
}这一步中lua虚拟机的该对象如果没有全部置空,比如这个UI table 是个全局变量或者其他UI任然持有,那么整个Lua以及C#对象都将无法释放。
之后释放掉C#对 这个 ui table的引用。有两种办法:一、直接调用Dispose方法,二、等待C#的GC,在LuaTable这个class析构方法中对引用进行释放,因为资源是立即销毁的,所以推荐立刻调用Dipose方法。
如果C#这边的引用也为空,内存状态
最后调用下C#这边的GC.Collec() 方法,OK这些对象才被最终释放干净。
总结一下流程:
听到我上面讲述的整个释放过程是不是被绕晕了?而实际中只有这么一种操作手段,没有什么其他更好的办法了,所以这部分的内存极其容易泄露。
3、没有什么好的办法查看lua这边具体的函数耗时以及GC消耗,导致程序员知道自己的函数内存或者时间消耗不是太理想,却又不知道怎么去优化。
Unity的Profiler就可以看到很多C#这边的GC消耗量,定位到问题函数非常快。Lua这边就非常麻烦,搞的程序员知道时间消耗在Lua这边,可是定位却非常麻烦,就算偶尔优化一下,隔个几天效率问题又出现了,优化方案很难确定下来。
那么这些问题应该如何解决呢?我这里安利我自己写的一款工具LuaProfiler-For-Unity
它可以将C#和Lua的整个函数调用过程展示在你面前:
以及函数当前的GC、消耗的时间、调用的次数,全部统计到你的面前。Lua内存、Mono内存、Android上的Pss、Ref表的lua对象、全部可以展现到你的面前:
帮助你快速的定位哪些函数GC过量,哪些函数有可能存在内存泄露、哪些函数调用时间过长等等问题。知道问题代码制定解决方案自然非常简单。
支持在运行过程中Record出来一段函数进行,具体分析。
支持真机调试
两个时间段变量DIFF
选择一个合适的时间段,比如UI开启前点击MarkLuaRecord,将出现以下记录
点击DiffRecord即可在之后的记录中与Record的时间段进行Diff操作, add 标签下为新增变量,rm下为被删除的变量, 点击记录后面的detail按钮可以显示具体是哪些地方引用了这个变量。Destory null Value为C#为空,而Lua的引用不为空的对象,需要重点关注。
判断对象C#为空而Lua不为空的指令:
obj:Equals(nil)R表获取,Lua代码中可以使用函数获取到,ToLua C#引用的对象都会在_R这张表上有个弱引用(就是其他地方全部为空,那么我这张表里的对象可以被GC掉),如果 null object很多在 _R.4.x上的话呢,可以使用GCDiff这个功能,Diif前先执行一遍GC,再看看对象是否正常释放....
debug.getregistry()优化关注点
1、产生Lua GC的函数
2、总内存量不停上涨的函数
3、时长消耗比较久的函数
4、执行销毁操作后,R表是否正常释放
5、C#为空,而Lua不为空的对象要在Lua这边置空
更多功能请上GIT上下载一个研究一下,别忘了Star一下哦。
github地址:
ElPsyCongree/LuaProfiler-For-UnityQQ群号:882425563

敢想敢做敢拼 发表于 2021-1-13 09:10

talking is expensive, show you the code:
---@type LuaProfiler
local LuaProfiler = class("LuaProfiler")

local isEnabled = Constants.DebugExecTime
function LuaProfiler:ctor(name)
        if not isEnabled then return end
        self.name = name
        self.totalTimeRecorder = {}
        self.timestamp = 0
end

function LuaProfiler:RecordBegin(funcName)
        if not isEnabled then return end       
        self.funcName = funcName

        local record = self.totalTimeRecorder
        if record == nil then
                self.totalTimeRecorder = {call=0,totaltime=0,max=0,min=9999,records={}}
                record = self.totalTimeRecorder
        end
        record.timestamp = Time.realtimeSinceStartup
end

function LuaProfiler:RecordEnd(funcName,recordToList)
        if not isEnabled then return end
       
        if not funcName then
                if not self.funcName then return end
                funcName = self.funcName
                self.funcName = nil       
        end

        local record = self.totalTimeRecorder
        if not record then return end

        record.timestamp = (Time.realtimeSinceStartup-record.timestamp)*1000                               
        record.call = record.call + 1
        record.totaltime = record.totaltime + record.timestamp                               
        if record.max < record.timestamp then record.max = record.timestamp end
    if record.min > record.timestamp then record.min = record.timestamp end
    if recordToList then table.insert(record.records, record.timestamp) end
    return record.timestamp
end

function LuaProfiler:AddStaticMoniter(clz, funcName, recordAll)
        if clz then
                local func = clz
                local s = clz.__cname.."."..funcName
                clz = function (...)
                        self:RecordBegin(s)
                        func(...)
                        self:RecordEnd(s, recordAll)                       
                end
        end
end

function LuaProfiler:AddMonitor(clzInstance, funcName, recordAll)

        local clz = clzInstance.class or clzInstance
        if type(funcName) == "string" then
                local func = clz
                local s = clz.__cname.."."..funcName
                local f = function(...)
                        self:RecordBegin(s)
                        local ret = {func(...)}
                        self:RecordEnd(s, recordAll)               
                        return table.unpack(ret)
                end

                if clz.class then
                        clz.class = f
                else
                        clz = f
                end
        else               
                for k,v in pairs(clz) do
                        if type(v) == "function" then
                                local s = clz.__cname.."."..k
                                local func = function(...)
                                        self:RecordBegin(s)
                                        local ret = {v(...)}
                                        self:RecordEnd(s, recordAll)               
                                        return table.unpack(ret)
                                end
                                if clz.class then
                                        clz.class = func
                                else
                                        clz = func
                                end
                        end
                end
        end
end

function LuaProfiler:SaveProfile()
        if not isEnabled then return end
        for k,v in pairs(self.totalTimeRecorder) do
          v.average = v.totaltime / v.call
          v.func = k
        end
        local tobesort = table.values(self.totalTimeRecorder)
        table.sort(tobesort, function(a,b)
          return a.average > b.average
        end)
        if not CS.System.IO.Directory.Exists(Application.persistentDataPath.."/profilers/") then
                CS.System.IO.Directory.CreateDirectory(Application.persistentDataPath.."/profilers/")
        end
        local logger = CS.LogFileRecorder(Application.persistentDataPath.."/profilers/"..self.name..".csv")
        logger:WriteLine("Function,Average Time(ms),Max Time(ms),Min Time(ms),Total Time(ms),Call Times,Records...")
        for i,v in ipairs(tobesort) do
          logger:WriteLine(string.format("%s,%.03f,%.03f,%.03f,%.03f,%d,%s",v.func,v.average,v.max,v.min,v.totaltime,v.call,string.join(v.records,",")))
        end
        logger:Close()
end

local recorders = {}
function LuaProfiler.Get(key)
        if not recorders then
                recorders = LuaProfiler.new(key)
        end

        return recorders
end

function LuaProfiler.SaveAll()
        for k,v in pairs(recorders) do
                v:SaveProfile()
        end
end

function LuaProfiler.SaveToFile(txtContent,fileName)
        local logger = CS.LogFileRecorder(Application.persistentDataPath.."/"..fileName..".txt")
        logger:WriteLine(txtContent)
        logger:Close()
end

return LuaProfiler用法:
--加入监听
LuaProfiler.Get("profiler_UIDefaultServer"):AddMonitor(require("UI.UIDefaultServer"))

--监听完成时生成报告:
LuaProfiler.SaveAll() 生成如下:
不解释了~看代码吧。可以指定监听特定类或者特定的方法,并生成报告,结合EXCEL的排序、筛选功能,这种方式我们自己用起来还挺顺手的,希望能帮到题主

计划你大爷计j 发表于 2021-1-13 09:10

https://github.com/ElPsyCongree/LuaProfiler-For-Unity去试试吧

幸福341 发表于 2021-1-13 09:17

内存量L下面的l_g有记录当前lua使用的内存量,lua中也有对应的api。性能分析复杂一些,知乎上之前有大佬分享过lua性能分析的方法。基本思路就是改一下lua function的Proto,在call和endcall之间打标记记录每个函数调用时间,在处理一下tailcall的问题就好了。要在移动端看的话,最好能接个函数到lua代码中,每帧能获取到当前帧调用的所有lua函数和函数调用时间,c#里简单弄个tcp发到pc端看就可以了。(ps:uwe好像有解决方案了)
lua很费的,做热更内容还行,要是大量每帧调用的函数放到lua里面估计会卡爆...我们重lua逻辑的项目,也只有屏幕遥杆控制在lua,其他该c#的还是得c#。

内托体头 发表于 2021-1-13 09:18

导致cpu增高的地方就是,可以打印函数执行时间来检查。

123456879 发表于 2021-1-13 09:23

关注一下lua和C#之间引用对象的数量。这个对象数量如果有很多,那么性能就会出现问题。
页: [1]
查看完整版本: 用Unity+Lua开发游戏,有什么好的办法进行性能检测?