找回密码
 立即注册
查看: 598|回复: 6

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

[复制链接]
发表于 2021-1-13 08:57 | 显示全部楼层 |阅读模式
用Unity+Lua开发游戏,有什么好的办法进行性能检测?
发表于 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,并不知道两个脚本交互的时候细节上的损耗到底有多大。
例如下面几个很普通的调用
  1. Transform child = transform.find("children1");
  2. transform.position = new Vector3(0,1,0)
  3. transfrom.gameObject.name = "1234";
复制代码
在C#那边几乎没有性能问题,但是在lua这边因为字符串传递的消耗,以及lua这边会把Vector3当成table或者userdata处理,很容易就会因为开发者的疏忽从而导致一些意想不到的瓶颈问题。
C#的tostring函数
  1. public static string lua_tostring(IntPtr L, int index)
  2. {
  3.     IntPtr strlen;
  4.     IntPtr str = lua_tolstring(L, index, out strlen);
  5.     if (str != IntPtr.Zero)
  6.         {
  7.         // 这里new了一个字符串
  8.         string ret = Marshal.PtrToStringAnsi(str, strlen.ToInt32());
  9.         if (ret == null)
  10.         {
  11.             int len = strlen.ToInt32();
  12.             // 这里new 了一个byte数组
  13.             byte[] buffer = new byte[len];
  14.             Marshal.Copy(str, buffer, 0, len);
  15.             // 这里new 了一个字符串
  16.             return Encoding.UTF8.GetString(buffer);
  17.         }
  18.         return ret;
  19.     }
  20.     else
  21.         {
  22.         return null;
  23.         }
  24. }
复制代码
所以,一定要避免字符串频繁传递,尤其是UI打开的时候获取一些控件,千万别用字符串来。应该在C#那边组织好(比如在prefab中维护一个public List<UnityEngine.Object>,然后直接把对应的对象拽进去),然后通过LuaTable注册给UI操作table。
XLua的 push_struct方法
  1. LUA_API void *xlua_pushstruct(lua_State *L, unsigned int size, int meta_ref) {
  2.     // 在Lua这边new了一个 userdata
  3.     CSharpStruct *css = (CSharpStruct *)lua_newuserdata(L, size + sizeof(int) + sizeof(unsigned int));
  4.     css->fake_id = -1;
  5.     css->len = size;
  6.     lua_rawgeti(L, LUA_REGISTRYINDEX, meta_ref);
  7.     lua_setmetatable(L, -2);
  8.     return css;
  9. }
复制代码
一定要避免在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的引用
  1. static void DisposeUI(string uiName)
  2. {
  3.     // Destory Object 将ui的LuaTable的置空掉
  4. }
复制代码
这一步中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不为空的指令:
  1. obj:Equals(nil)
复制代码
R表获取,Lua代码中可以使用函数获取到,ToLua C#引用的对象都会在_R[4]这张表上有个弱引用(就是其他地方全部为空,那么我这张表里的对象可以被GC掉),如果 null object很多在 _R.4.x上的话呢,可以使用GCDiff这个功能,Diif前先执行一遍GC,再看看对象是否正常释放....
  1. 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:
  1. ---@type LuaProfiler
  2. local LuaProfiler = class("LuaProfiler")
  3. local isEnabled = Constants.DebugExecTime
  4. function LuaProfiler:ctor(name)
  5.         if not isEnabled then return end
  6.         self.name = name
  7.         self.totalTimeRecorder = {}
  8.         self.timestamp = 0
  9. end
  10. function LuaProfiler:RecordBegin(funcName)
  11.         if not isEnabled then return end       
  12.         self.funcName = funcName
  13.         local record = self.totalTimeRecorder[funcName]
  14.         if record == nil then
  15.                 self.totalTimeRecorder[funcName] = {call=0,totaltime=0,max=0,min=9999,records={}}
  16.                 record = self.totalTimeRecorder[funcName]
  17.         end
  18.         record.timestamp = Time.realtimeSinceStartup
  19. end
  20. function LuaProfiler:RecordEnd(funcName,recordToList)
  21.         if not isEnabled then return end
  22.        
  23.         if not funcName then
  24.                 if not self.funcName then return end
  25.                 funcName = self.funcName
  26.                 self.funcName = nil       
  27.         end
  28.         local record = self.totalTimeRecorder[funcName]
  29.         if not record then return end
  30.         record.timestamp = (Time.realtimeSinceStartup-record.timestamp)*1000                               
  31.         record.call = record.call + 1
  32.         record.totaltime = record.totaltime + record.timestamp                               
  33.         if record.max < record.timestamp then record.max = record.timestamp end
  34.     if record.min > record.timestamp then record.min = record.timestamp end
  35.     if recordToList then table.insert(record.records, record.timestamp) end
  36.     return record.timestamp
  37. end
  38. function LuaProfiler:AddStaticMoniter(clz, funcName, recordAll)
  39.         if clz[funcName] then
  40.                 local func = clz[funcName]
  41.                 local s = clz.__cname.."."..funcName
  42.                 clz[funcName] = function (...)
  43.                         self:RecordBegin(s)
  44.                         func(...)
  45.                         self:RecordEnd(s, recordAll)                       
  46.                 end
  47.         end
  48. end
  49. function LuaProfiler:AddMonitor(clzInstance, funcName, recordAll)
  50.         local clz = clzInstance.class or clzInstance
  51.         if type(funcName) == "string" then
  52.                 local func = clz[funcName]
  53.                 local s = clz.__cname.."."..funcName
  54.                 local f = function(...)
  55.                         self:RecordBegin(s)
  56.                         local ret = {func(...)}
  57.                         self:RecordEnd(s, recordAll)               
  58.                         return table.unpack(ret)
  59.                 end
  60.                 if clz.class then
  61.                         clz.class[funcName] = f
  62.                 else
  63.                         clz[funcName] = f
  64.                 end
  65.         else               
  66.                 for k,v in pairs(clz) do
  67.                         if type(v) == "function" then
  68.                                 local s = clz.__cname.."."..k
  69.                                 local func = function(...)
  70.                                         self:RecordBegin(s)
  71.                                         local ret = {v(...)}
  72.                                         self:RecordEnd(s, recordAll)               
  73.                                         return table.unpack(ret)
  74.                                 end
  75.                                 if clz.class then
  76.                                         clz.class[k] = func
  77.                                 else
  78.                                         clz[k] = func
  79.                                 end
  80.                         end
  81.                 end
  82.         end
  83. end
  84. function LuaProfiler:SaveProfile()
  85.         if not isEnabled then return end
  86.         for k,v in pairs(self.totalTimeRecorder) do
  87.             v.average = v.totaltime / v.call
  88.             v.func = k
  89.         end
  90.         local tobesort = table.values(self.totalTimeRecorder)
  91.         table.sort(tobesort, function(a,b)
  92.             return a.average > b.average
  93.         end)
  94.         if not CS.System.IO.Directory.Exists(Application.persistentDataPath.."/profilers/") then
  95.                 CS.System.IO.Directory.CreateDirectory(Application.persistentDataPath.."/profilers/")
  96.         end
  97.         local logger = CS.LogFileRecorder(Application.persistentDataPath.."/profilers/"..self.name..".csv")
  98.         logger:WriteLine("Function,Average Time(ms),Max Time(ms),Min Time(ms),Total Time(ms),Call Times,Records...")
  99.         for i,v in ipairs(tobesort) do
  100.             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,",")))
  101.         end
  102.         logger:Close()
  103. end
  104. local recorders = {}
  105. function LuaProfiler.Get(key)
  106.         if not recorders[key] then
  107.                 recorders[key] = LuaProfiler.new(key)
  108.         end
  109.         return recorders[key]
  110. end
  111. function LuaProfiler.SaveAll()
  112.         for k,v in pairs(recorders) do
  113.                 v:SaveProfile()
  114.         end
  115. end
  116. function LuaProfiler.SaveToFile(txtContent,fileName)
  117.         local logger = CS.LogFileRecorder(Application.persistentDataPath.."/"..fileName..".txt")
  118.         logger:WriteLine(txtContent)
  119.         logger:Close()
  120. end
  121. return LuaProfiler
复制代码
用法:
  1. --加入监听
  2. LuaProfiler.Get("profiler_UIDefaultServer"):AddMonitor(require("UI.UIDefaultServer"))
  3. --监听完成时生成报告:
  4. LuaProfiler.SaveAll()
复制代码
生成如下:
不解释了~看代码吧。可以指定监听特定类或者特定的方法,并生成报告,结合EXCEL的排序、筛选功能,这种方式我们自己用起来还挺顺手的,希望能帮到题主

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2021-1-13 09:10 | 显示全部楼层
https://github.com/ElPsyCongree/LuaProfiler-For-Unity去试试吧
发表于 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增高的地方就是,可以打印函数执行时间来检查。
发表于 2021-1-13 09:23 | 显示全部楼层
关注一下lua和C#之间引用对象的数量。这个对象数量如果有很多,那么性能就会出现问题。
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-10 13:40 , Processed in 0.094295 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表