赞
踩
Transform.SetParent
GameObject.Deactivate/Active
MipmapVisualization
Unity3D性能优化:ShaderLab内存占用
前言:
手机的内存占用一直是项目优化的重要部分。最近优化项目的内存占用时。发现了个占用比较恐怖的地方。
那就是ShaderLab
正文:
从上图(进入战斗场景时的内存快照)可以看出,ShaderLab占用居然达到42M(请忽略其他项数据,因为还没优化到=。=)。
为什么Shader的占用那么高呢?
由于当前项(ShaderLab)没有说明详细的shader占用信息。所以只能去另外找原因了,还好,在内存快照的Assets下的shader项中,有详细的使用信息。
然后我看到Standard的shader使用。那么我就开始迷惑了,因为项目里面根本没有地方使用过Standard这个shader,为什么会存在呢。
所以我感觉内存占用的大头就出在这了。
然后进行了一轮的排查,把部分使用到Standard的地方清除掉,测试并再拿一次内存快照。
清除了一部分后,ShaderLab降到27.6M(后来完全清除掉Standard,降到21M)。果然,主要原因就出在了Standard上。
那么问题又来了,为什么我没有使用到Standard,却会在内存中看到Standard的使用呢?
这里就要说下两个坑了,同样,也是排查这个问题的方法。
一.模型导入导致。
模型导入的时候,“Import Materials”是默认勾选的。所以当模型导入时,Unity会在同目录创建“Materials”目录,并创建相应的材质,而这个材质默认是使用Standard。
由于美术在制作过程中对Prefab中对模型另外赋予材质,所以实际上,默认创建的材质(Standard)是没有使用到的。可是当加载模型的时候,却又把默认创建的材质加载上了,并对着色器解析了。因此导致内存中有Standard。
那么解决方法也很简单,把“Import Materials”去掉,并把没有使用的默认材质删除。
注意:如果不把“Import Materials”去掉,导入到其他项目时,材质又会自动创建了。
由于实际项目中,Prefab改动次数比较大,相应的模型文件改动比较少,所以把项目中的模型和对应的Prefab分开打包成不同的AssetBundle。然后就出现一个很奇怪的情况。
没有勾选“Import Materials”的模型文件,在实例化Prefab时,ShaderLab会存在一份“Standard”的shader内存,而这个shader的引用是指向一
个“Default-Material”文件(可是这文件并不存在)。
但是,在模型和Prefab在相同的AssetBundle中,或者使用Resources加载时,却不会有“Standard”和“Default-Material”的出现。
暂时还还不确定是这是版本bug,还是Unity的特殊机制。(版本5.3.3)
临时解决方案:在需要模型与Prefab分开打包时,勾选“Import Materials”,直接使用和修改默认生成的材质。
二.默认模型(Cube、Sphere)创建导致的。
早期场景搭建时,为了方便定位和可视化,曾经使用Cube等系统默认的Mesh作为锚点,然后在启动游戏时禁用掉。
由于这些Cube不启用,性能消耗很轻微。所以就没有理会了。
可是,就因为是系统默认的Mesh,所以创建时,赋予的材质就是默认的材质“Default-Material”,而这个材质使用的着色器就恰恰是“Standard”。
所以“Standard”存在的并不冤了。
解决的方案也很简单,删掉这些mesh或者是替换材质。这样,这部分占用的“Standard”就不存在了。
总结:
由于Standard的变体太多了,所以当引用了Standard的时候,往往会存在多个Standard变体,占用大量的内存。如果,你发觉你的ShaderLab的内存过大,而又那么也不妨找找是不是上述的原因。
那ShaderLab占用内存过大是不是完全是Standard的原因呢?其实并不止的。像我优化完之后的27M(完全清除后是21M),肯定还有其他原因造成的。可是,优化的过程是砍大头,像上文那样,稍微优化一下,就能拿掉20多M,当然要立刻做,可是越往小的时候,优化效率就越来越低了,所以这时候就需要转移目标看一下“Objects”“Texture2D”这些大头了。所以,剩下ShaderLab的优化方向可能会在以后遇到的时候再补充。
使用Unity的FrameDebugger简单分析
使用Unity MemoryProfiler分析内存占用和内存泄漏
通过memoryprofile抓帧分析,内存占用不高,但是RenderTexture占用非常高63.5M
使用XCode做详尽的性能优化
XCode也可以做GPU的性能分析。编译xcode工程需要Development版,启动,GUP的优化点需要在Run的面板里设置成metal否则有些堆栈看不到。点FPS,然后点相机Capture一帧,可以看到帧率,CPUGPU的占比,渲染时每个函数的开销。点Shader里面的看更详细堆栈,可以看到每个shader的每行代码的开销占比。
渲染线程开销也较高
其他一些测试以及和Unity官方支持的问答:
CPU
使用Profile找到CPU占用最靠前的函数,从最高的开始依次分析优化。定位的方法有很多,Unity的Profile,UWA的性能测试工具,比较推荐的是使用XCode,可以抓取一段时间内函数的开销。更品均准确也可以看到更底层。
如过函数比较复杂可以使用BeginSample/EndSample拆分,我在项目里通过添加条件编译进行了封装类似:MDebug.BeginSample("Character.TakeDamage");
这样可以跟随意和高效的定位到关心的地方。下面列举部分比较通用的优化方案。
最常用的优化方式。例如屏幕外的角色不计算动画更新,不计算技能效果冒字血条等,屏幕外的角色休眠,只有主角才会冒字等等。还可以做一些LOD,例如AI可以做行为树lod,动画LOD,粒子特效LOD,更新频率LOD(更新频率随着游戏帧率,离主角的距离,重要程度,视锥,类似CSM的提醒分段,以及周围的角色个数动态调整。例如轩辕城擂台,同屏高配200人,未优化在68帧左右优化的后到94帧左右)
为了降低耗电发热量我们会根据玩家机型进行限帧。另一方面我们将逻辑帧和渲染帧分开执行,逻辑代码以更低的帧率执行,部分逻辑也可以使用线程,负载均衡分段计算等方法提升性能。
一些代码本身的运算。例如优化物理运算,空间换时间使用查表预计算等方式对计算提速,减少频繁索引FindGetComponentd以及各种运算,优化遍历利用稀疏矩阵九宫四叉树等等。
根据不同类型的游戏也会调整一些运算是在服务器还是客户端,如果服务器性能强大可以让服务器计算物理,寻路,AI,战斗逻辑等复杂运算,客户端只要变现即可,特别是MMO的项目。而如果希望服务器开发较少提升开发效率和降低服务器性能要求,也可以将绝大部分运算放在客户端,例如帧同步的游戏我们就采取的这种方式。当然有时候也是两者相结合并没有绝对标准。
Unity使用PhysX作为物理引擎,本身优化还是很好的,会做空间划分。Unity官方建议碰撞对小于100,其实这个标准非常严苛,我们测试在300左右物理的开销还是蛮少。优化方面可以通过分层减少碰撞对,尽量使用BoxCollider而不是MeshCollider,UI界面不需要点击的控件不要打开Raycaster。因为我们只使用了最基本的射线检测,其他物理是自己实现的,主要的优化还是在物理算法上。如过在Profile中发现Physucs.Simulate开销比较大就是物理需要优化了。
把Unity编译设置成IL2CPP,编译成C++版运行效率会有较大提升。还可以把一些运算逻辑放到C++的库里,这样可以优化更极致减少gc。
如过Prifile中发现Animator.Update或者MeshSkinning.Udpate开销比较大就说明动作可能需要优化。
UI也是个开销大头,一般会占到30%-50%。UGUI对应Profile中Canvas.BuildBatch &
Canvas.SendWillRenderCanvases开销,类似NGUI的LastUpdate,UI的优化又很多文章这里也简单列举一下。
GC是一个非常高开销的系统调用也,是大部分卡顿的主要原因,不能完全控制。因此我们要尽量减少代码堆内存分配过量防止频繁触发GC,同时也可以在Loading或者对性能不敏感的时候主动GC。
GPU
DrawCall实际上优化的CPU的时间,但因为DC的优化一般都是材质mesh合并,所以放到了GPU的部分。每次在准备数据并通知GPU渲染的过程称为一次Draw
Call。渲染一次拥有一个网格并携带一种材质的物体便会使用一次Draw Call。可以理解为调用一次DC就换一种画笔在画板上画一个物体。
限于篇幅,虽然多线程是CPU的部分但都放在GPU这部分介绍。随着PC的主频达到瓶颈,手机也开始朝多核并行开发的方向前进了。所幸Unity这方面做的比较好,大部分开发者可以不用考虑多线程的开发方式。
DC<200,面数小于10w是Unity建议。我在14年的测试,在红米1上,当dc超过100性能开始直线下降,面数超过6w面性能开始直线下降。
我们目前场景的标准是整个场景少于10个DC,由美术或插件合并输出,这样虽然不是十分灵活,但因为静态合并会增加Loading时间和内存,动态合并也会增加内存和合并CPU开销,所以还是采取这种优化方式。角色2000面左右,低配1个DC,高配3个DC(描边,实时阴影,主角还有额外的一个因为墙后半透效果)
对GPU的优化也可以通过LOD进行,可以通过模型LOD,骨骼LOD,粒子LOD,材质LOD的方式,地形LOD等等,例如不同配置开启不同的效果,开启后处理等。
相对不透明半透明开销巨大,在PC和手游上都是,还会破化渲染管线的优化。另外使用alpha通道的贴图压缩也很困难(特别是IOS上的PVR格式Alpha像素压缩之后损失巨大,ETC,DDS,PVR等格式Alpha一个通道的压缩比就等于其他3个通道了)。半透明物体不写Z无法做像素级遮挡,需要做混合操作,半透明需要单独排序。以下有一些比较通用的优化点;
这个其实也是优化的CPU。每个相机都会针对场景图做Culling,通过视锥和包围盒剔除这个摄像机看不见的物体,减少实际渲染物体个数。因为Culling操作比较耗时,也可以通过减少摄像机下对象的个数或者手工开关分层来做优化。这里需要注意的是,合并方式也会影响Culling,例如把整个游戏所有的树的都合并成一个DC,DC是下降了,但是只要有一棵树在摄像机里,所有合并的树模型都会被渲染,增大了渲染的带宽和负载需要权衡使用。例如手游穿越火线就是把整个场景优化成了3个DC,一个DC渲染所有地形,一个渲染所有的墙,一个渲染所有的箱子等。
内存
其实在手机上内存的优化才是最重要的,绝大部分闪退都是内存不足导致,都闪退了其他优化还有什么用呢?内存分析工具有很多,使用Unity的Profile,memoryprofile,XCode,,UWA。一般简单快速分析用Profile,具体内存资源使用memoryprofile和uwa,内存泄露代码级内存优化使用xcode。
下面介绍一些具体的优化方式。
闪存
在PC上也就是硬盘。具体表现在包大小,资源加载速度等。闪存的优化和内存大部分共同,但是也有例外,例如使用jpg就是减少包大小增大内存消耗CPU的方法。
网络
耗电
耗电可能不会被大家所重视,但其实在手游上这是一个非常重要的指标。如果游戏打一两个小时就没电了,那出门在外谁敢玩呢?火遍全国的王者荣耀就在省电上做了非常多的优化,我们游戏也是。那么有哪些优化方案呢?
1. CPU
A. WaitForTargetFPS:
Vsync(垂直同步)功能所,即显示当前帧的CPU等待时间
B. Overhead:
Profiler总体时间-所有单项的记录时间总和。用于记录尚不明确的时间消耗,以帮助进一步完善Profiler的统计。
C. Physics.Simulate:
当前帧物理模拟的CPU占用时间。
D. Camera.Render:
相机渲染准备工作的CPU占用量
E. RenderTexture.SetActive:
设置RenderTexture操作.
底层实现:1.比对当前帧与前一帧的ColorSurface和DepthSurface.
2.如果这两个Buffer一致则不生成新的RT,否则则生成新的RT,并设置与之相对应的Viewport和空间转换矩阵.
F. Monobehaviour.OnMouse_ :
用于检测鼠标的输入消息接收和反馈,主要包括:SendMouseEvents和DoSendMouseEvents。(只要Edtor开起来,这个就会存在)
G. HandleUtility.SetViewInfo:
仅用于Editor中,作用是将GUI和Editor中的显示看起来与发布版本的显示一致。
H. GUI.Repaint:
GUI的重绘(说明在有使用原生的OnGUI)
I. Event.Internal_MakeMasterEventCurrent:
负责GUI的消息传送
J. Cleanup Unused Cached Data:
清空无用的缓存数据,主要包括RenderBuffer的垃圾回收和TextRendering的垃圾回收。
1.RenderTexture.GarbageCollectTemporary:存在于RenderBuffer的垃圾回收中,清除临时的FreeTexture.
2.TextRendering.Cleanup:TextMesh的垃圾回收操作
K. Application.Integrate Assets in Background:
遍历预加载的线程队列并完成加载,同时,完成纹理的加载、Substance的Update等.
L. Application.LoadLevelAsync Integrate:
加载场景的CPU占用,通常如果此项时间长的话70%的可能是Texture过长导致.
M. UnloadScene:
卸载场景中的GameObjects、Component和GameManager,一般用在切换场景时.
N. CollectGameObjectObjects:
执行上面M项的同时,会将场景中的GameObject和Component聚集到一个Array中.然后执行下面的Destroy.
O. Destroy:
删除GameObject和Component的CPU占用.
P. AssetBundle.LoadAsync Integrate:
多线程加载AwakeQueue中的内容,即多线程执行资源的AwakeFromLoad函数.
Q. Loading.AwakeFromLoad:
在资源被加载后调用,对每种资源进行与其对应用处理.
2.GPU Usage
A. Device.Present:
device.PresentFrame的耗时显示,该选项出现在发布版本中.
B. Graphics.PresentAndSync:
GPU上的显示和垂直同步耗时.该选项出现在发布版本中.
C. Mesh.DrawVBO:
GPU中关于Mesh的Vertex Buffer Object的渲染耗时.
D. Shader.Parse:
资源加入后引擎对Shader的解析过程.
E. Shader.CreateGPUProgram:
根据当前设备支持的图形库来建立GPU工程.
3. Memory Profiler
A. Used Total:
当前帧的Unity内存、Mono内存、GfxDriver内存、Profiler内存的总和.
B. Reserved Total:
系统在当前帧的申请内存.
C. Total System Memory Usage:
当前帧的虚拟内存使用量.(通常是我们当前使用内存的1.5~3倍)
D. GameObjects in Scene:
当前帧场景中的GameObject数量.
E. Total Objects in Scene:
当前帧场景中的Object数量(除GameObject外,还有Component等).
F. Total Object Count:
Object数据 Asset数量.
4. Detail MemoryProfiler
A. Assets:
Texture2d:记录当前帧内存中所使用的纹理资源情况,包括各种GameObject的纹理、天空盒纹理以及场景中所用的Lightmap资源.
B. Scene Memory:
记录当前场景中各个方面的内存占用情况,包括GameObject、所用资源、各种组件以及GameManager等(天般情况通过AssetBundle加载的不会显示在这里).
A. Other:
ManagedHeap.UseSize:代码在运行时造成的堆内存分配,表示上次GC到目前为止所分配的堆内存量.
SerializedFile(3):
WebStream:这个是由WWW来进行加载的内存占用.
System.ExecutableAndDlls:不同平台和不同硬件得到的值会不一样。
5. 优化重点
A. CPU-GC Allow:
关注原则:1.检测任何一次性内存分配大于2KB的选项 2.检测每帧都具有20B以上内存分配的选项.
B. Time ms:
记录游戏运行时每帧CPU占用(特别注意占用5ms以上的).
C. Memory Profiler-Other:
1.ManagedHeap.UsedSize: 移动游戏建议不要超过20MB.
2.SerializedFile: 通过异步加载(LoadFromCache、WWW等)的时候留下的序列化文件,可监视是否被卸载.
3.WebStream: 通过异步WWW下载的资源文件在内存中的解压版本,比SerializedFile大几倍或几十倍,重点监视.****
D. Memory Profiler-Assets:
1.Texture2D: 重点检查是否有重复资源和超大Memory是否需要压缩等.
2.AnimationClip: 重点检查是否有重复资源.
3.Mesh: 重点检查是否有重复资源.
6. 项目中可能遇到的问题
A. Device.Present:
1.GPU的presentdevice确实非常耗时,一般出现在使用了非常复杂的shader.
2.GPU运行的非常快,而由于Vsync的原因,使得它需要等待较长的时间.
3.同样是Vsync的原因,但其他线程非常耗时,所以导致该等待时间很长,比如:过量AssetBundle加载时容易出现该问题.
4.Shader.CreateGPUProgram:Shader在runtime阶段(非预加载)会出现卡顿(华为K3V2芯片).
B. StackTraceUtility.PostprocessStacktrace()和StackTraceUtility.ExtractStackTrace():
1.一般是由Debug.Log或类似API造成.
2.游戏发布后需将Debug API进行屏蔽.
C. Overhead:
1.一般情况为Vsync所致.
2.通常出现在Android设备上.
D. GC.Collect:
原因: 1.代码分配内存过量(恶性的) 2.一定时间间隔由系统调用(良性的).
占用时间:1.与现有Garbage size相关 2.与剩余内存使用颗粒相关(比如场景物件过多,利用率低的情况下,GC释放后需要做内存重排)
E. GarbageCollectAssetsProfile:
1.引擎在执行UnloadUnusedAssets操作(该操作是比较耗时的,建议在切场景的时候进行).
2.尽可能地避免使用Unity内建GUI,避免GUI.Repaint过渡GCAllow.
3.if(other.tag == GearParent.MogoPlayerTag)改为other.CompareTag(GearParent.MogoPlayerTag).因为other.tag为产生180B的GCAllow.
F. 少用foreach,因为每次foreach为产生一个enumerator(约16B的内存分配),尽量改为for.
G. Lambda表达式,使用不当会产生内存泄漏.
H. 尽量少用LINQ:
1.部分功能无法在某些平台使用.
2.会分配大量GC Allow.
I. 控制StartCoroutine的次数:
1.开启一个Coroutine(协程),至少分配37B的内存.
2.Coroutine类的实例 —21B.
3.Enumerator —16B.
J. 使用StringBuilder替代字符串直接连接.
K. 缓存组件:
1.每次GetComponent均会分配一定的GC Allow.
2.每次Object.name都会分配39B的堆内存.
//
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。