导图社区 Unity性能优化最佳攻略
Unity不断发展与演变,因此旧技巧可能不再是提升引擎性能的最佳方法。分享改进性能和优化的技巧,这些技巧反映了Unity支持面向数据设计的架构的演变。
编辑于2019-12-12 08:06:03unity性能优化
简述
帧率
衡量游戏性能的基本指标
一帧
绘制到屏幕上的一个静止画面
每秒的帧数(fps)或者说帧率表示GPU处理时每秒钟能够更新的次数。高的帧率可以得到更流畅、更逼真的动画
最低30FPS
提升游戏性能,要看渲染一帧需要多少毫秒
公式: 1000/(想要达到的帧率)。通过这个公式可以得到,30FPS必须在33.3毫秒之内渲染完一帧,60FPS必须在16.6毫秒内渲染完一帧。
1000是1000毫秒也就是1秒
如,从60到50FPS呈现出的是额外3.3毫秒的运行时间,但是从30到20FPS呈现出的是额外的16.6毫秒的运行时间。在这里,同样降低了10FPS,但是渲染一帧上时间的差别是很显著的
有些任务每一帧都会执行,包括执行脚本,运算光照等。此外还有许多操作在一帧执行多次,如物理运算。当执行不满足要求的时候,渲染一帧需要花费更多的时间,帧率就会下降。
工具
Unity自带的Unity Profile
CPU usage profiler
Total:当前任务的时间消耗占当前帧cpu消耗的时间比例。
Self:任务自身时间消耗占当前帧cpu消耗的时间比例。
Calls:当前任务在当前帧内被调用的次数。
GC Alloc:当前任务在当前帧内进行过内存回收和分配的次数。
Time ms:当前任务在当前帧内的耗时总时间。
Self ms:当前任务自身(不包含内部的子任务)时间消耗。
Timeline
cpu任务的执行顺序
哪个线程负责什么任务
观察目标
CPU
GC Allow
任何一次性内存分配大于2KB的选项
每帧都具有20B以上内存分配的选项
Time ms
注意占用5ms以上的选项
内存
Texture
检查是否有重复资源和超大内存是否需要压缩等
AnimationClip
重点检查是否有重复资源
Mesh
重点检查是否有重复资源
过程建议
可以关闭VSync垂直同步来提高帧率,然后追踪
处理尖点
IOS端的XCode Capture GPU frame
第三方插件,如腾讯推出的UPA性能分析工具
其他
实现
cpu
脚本
优化前提
寻找性能热点
目标设备去进行深度的profile
方面
untiy api
GameObject.GetComponent
高频使用,每次调用都会遍历所有的组件来找到目标组件
缓存来避免
Transform
内部提供了.transform来获取此组件,缓存的效率更高
图例
GameObject.Find
遍历所有的GameObject来返回名字相符的对象,游戏对象过多,会耗时
优化方式
在Star或者Awake的时,缓存一次找到的对象,后续使用缓存对象
GameObject.FindWithTag 寻找特定标签的对象
一开始就能确定的对象,可以考虑拖拽到Inspector中
Camera.main
unity通过GameObject.FindWithTag来找到tag来MainCamera的相机
需要频繁访问的时候,用缓存来提升
图例
GameObject.tag
直接用.tag == "xx" 来比较,每帧会产生GC Alloc
GameObject.CompareTag来比较,可以避免,前提是比较的tag在TagManager中定义过。
图例
MonoBehaviour
内部调用方法要管理起来,如Update函数,Unity会在每帧执行
创建manager用list管理所有的update,能有多倍效率提升
Transform.SetPositionAndRotation
调用Transform.SetPosition或者Transform.SetRotation,Unity会通知一遍所有的子节点
如果位置和角度预先知道,可以通过Transform.SetPostionAndRotation一次调用,避免两次调用的性能开销
Animator.Set…
Animator提供的SetTrigger、SetFloat等,如果传入字符串,字符串会被hash成一个整数
可以通过Animator.StringToHash来提前hash,避免每次的hash运算
Material.Set…
改变Shader同样需要hash计算,就可以提前通过Shader.PropertyToID进行hash
Vector Math
比较距离,不需要计算距离,可以不用开方做比较,用SqrMagnitude代替Magnitude
向量乘法
向量乘比较耗时,可以先整数乘,最后再和向量乘
图例
Coroutine
如果要进行定时操作,可以不再Update中进行每帧计算,用yield return new WaitForSecomds(1f)。用变量缓存 new WaitForSeconds(1f) 省去每次new的开销
SendMessage
内部采用了反射的实现机制,尽量避免使用,可以用事件代替
Debug.Log
发布的时候可以禁用掉Debug.logger.logEnabled = false
可以采用条件编译标签Conditional封装一层自己的Log输出
使用内建数组,如Vector3.zero而不是new Vector(0, 0, 0)
不要在发布版本中实现OnGUI方法
C#
反射
建立一个字符串-类型的字典来代替反射,或者采用delegate的方式来避免反射
内存分配(栈和堆)
在栈上分配的对象都是拥有固定大小的类型,在栈上分配内存十分高效
在堆上分配的对象都是不能确定其大小的类型,由于其内存大小不固定,所以经常容易产生内存碎片,导致其内存分配相对于栈来说更为低效
值类型和引用类型
值类型包括所有数字类型、Bool、Char、Date、所有Struct类型和枚举类型。其类型的大小都是固定 , 它们都在栈上进行内存分配
引用类型包括字符串、所有类型的数组、所有Class以及Delegate,它们都在堆上进行内存分配
图例
装箱、拆箱
Boxing指的是将值类型转换为引用类型,而拆箱UnBoxing的是将引用类型转换为值类型
装箱和拆箱存在着从栈到堆的互指以及堆内存的开辟,所以它们本质是一项非常耗时的操作,我们应该尽量避免之
图例
Mono之前的foreach导致每帧的GC Alloc,本质也是因为装箱和拆箱导致的,此问题已经在Unity5.6后被修复
GC
管理Mono托管堆
内存不足,自动GC
手动GC
堆上分配的内存,其实是由垃圾回收器(Garbage Collector)来负责回收的。垃圾回收算法异常耗时,因为它需要遍历所有的对象,然后找到没有引用的孤岛,将它们标记为「垃圾」,然后将其内存回收掉
频繁的垃圾回收不仅很耗时,还会导致内存碎片的产生,使得下一次的内存分配变得更加困难或者干脆无法分配有效内存,此时堆内存上限会往上翻一倍,而且无法回落,造成内存吃紧
极力避免GC Alloc,即需要控制堆内存的分配
字符串
字符串连接会导致GC Alloc
例如string gcalloc = "GC" + "Alloc"会导致"GC"变成垃圾,从而产生GC Alloc
如:string c = string.Format("one is {0}", 1),也会因为一次装箱操作(数字1被装箱成字符串"1")而产生额外的GC Alloc
StringBuilder类来专门进行字符串的连接
IL2CPP
Unity提供的将C#的IL码转换为C++代码的服务,由于转成了C++,所以其最后会转换成汇编语言,直接以机器语言的方式执行,而不需要跑在.NET虚拟机上,提高了性能
由于IL的反编译较为简单,转换成C++后,也会增加一定的反汇编难度
IL2CPP的C++代码虽然是自动生成的,但是其中间的某些过程也可以被人为操纵,从而达到提升性能的目的
Lua
在代码运行前,Lua会把源码预编译成一种中间码,类似于Java的虚拟机。这种格式然后会通过C的解释器进行解释,整个过程其实就是通过一个while循环,里面有很多的switch...case语句,一个case对应一条指令来解析
方式
使用local
多的寄存器,Lua的预编译器能把所有的local变量储存在其中,Lua在获取local变量时其效率十分的高
local a,b; a = a+b 寄存器图例
不使用local寄存器图例
方法用local图例
表table优化
一个lua表分为array部分和哈希hash部分,数组部分从1到n的整数键,其他的所有键都储存在哈希部分。
哈希部分其实一个数组,利用哈希算法将键转化为数组下标,若下表有冲突即同一个下标对应了两个不同的键,则它会将冲突的下标上创建一个链表,将不同的键串在这个链表上,这种解决冲突的方法叫做:链地址法
把一个新键值赋给表时,若数组和哈希表已经满了,则会触发一个再哈希rehash。再哈希的代价是高昂的。首先会在内存中分配一个新的长度的数组,然后将所有记录再全部哈希一遍,将原来的记录转移到新数组中。新哈希表的长度是最接近于所有元素数目的2的乘方
图例1
Lua创建了一个空表a,在第一次迭代中,a[1] = true触发了一次rehash,Lua将数组部分的长度设置为2^0,即1,哈希部分仍为空。在第二次迭代中,a[2] = true再次触发了rehash,将数组部分长度设为2^1,即2。最后一次迭代,又触发了一次rehash,将数组部分长度设为2^2,即4
图例2
其触发了三次表中哈希部分的rehash
图例3
创建非常多很小的表时,采用预先填充的方式,可以有成倍的效率提升
三个元素的表,2的2次方,在哈希部分创建长度为4的数组
关于字符串
lua中的字符串,均只有一份拷贝,如果出现新的字符串,会创建一份拷贝。比较字符串,只比较引用。
字符串连接问题
图例
大字符串连接,尽量避免“..”,模拟buffer用concat得到最终字符串
3R
减量化(Reducing)
如果程序中使用太多的表,可以考虑换一种数据结构来表示
把不需要循环的table放到循环外来创建
图例
再利用(Reusing)
重用旧对象,缓存内容,避免后续的重复计算
图例
再循环(Recycling)
Lua自带垃圾回收器,所以我们一般不需要考虑垃圾回收的问题。Lua的垃圾回收器是一个增量运行的机制。即回收分成许多小步骤(增量的)来进行
与c#交互
创建一个管理器继承MonoBehaviour的Update,派发给lua端,能节省大量时间
Lua和C#之间传参、返回时
传参需注意
严重类型
Vector3/Quaternion等Unity类型,数组
改成传值LuaUtil.SetPos(obj, pos.x, pos.y, pos.z),省掉了transform的频繁返回
次严重类型
bool string 各种object
建议传递
int float double
在Lua中引用C#的Object,代价昂贵
Lua拿着C#对象的引用时会造成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类
频繁调用的函数,不要超过4个参数
优先使用static函数导出,减少使用成员方法导出
一个object要访问成员方法或者成员变量,都需要查找Lua userdata和C#对象的引用,或者查找metatable,耗时甚多。直接导出static函数,可以减少这样的消耗
考虑在Lua中只使用自己管理的ID,而不直接引用C#的Object
合理利用out关键字返回复杂的返回值
数据结构
容器类型
经常需要进行随机下标访问的场合,优先选择数组(Array)或列表(List)
经常需要进行查找的场合,优先选择字典(Dictionary)
经常需要插入或删除的场合,优先选择链表(LinkedList)
不能存在相同元素的,可以选择HashSet
需要后进先出的,用来优化递归函数调用的,可以选择Stack
对象池
对象池(Object Pool)可以避免频繁的对象生成和销毁
解决:游戏对象的生成,首先需要开辟内存,其次还可能会引起GC Alloc,最后还可能会引发磁盘I/O。频繁的销毁对象会引发严重的内存碎片,使得堆内存的分配更加困难
图例
空间划分
解决:在计算空间碰撞或者寻找最近邻居时,如果空间很庞大,需要参与计算的对象太多的情况下,用两层循环逐个遍历去计算的复杂度为平方级
方式:借助于空间划分的数据结构来使复杂度降低到N*Log(N)。四叉树一般用来划分2D空间,八叉树一般用来划分3D空间,而KD树则是不限空间维度
算法
循环
循环的使用非常常见,也非常容易成为性能热点。我们应该尽量避免在循环内进行耗时或无效操作
解决Update中的无效操作图例
多重循环要把次数多的放在内层
数学运算
只比较距离,可以不用开方
巧用向量运算
缓存
缓存的本质就是用空间换时间。例如之前在Unity API中提到的很多耗时的函数,都可以用缓存来提升性能
渲染
drawcall
DrawCall:CPU对底层图形程序(比如:OpenGL ES)接口的调用,以在屏幕上画出东西
Batching:批处理就是将要多次调用(dc)的物体合并,之后只需要调用一次底层图形程序的接口就行
影响的CPU的效率,多次调用耗费CPU的量,不影响图形处理。解决此问题的主要思路就是减少渲染次数,多个物体一起渲染
方式
DrawCallBatching
Static Batching
如场景,只要物体不移动,并且拥有相同的材质,不关心大小
需要手动勾选Static
Dynamic Batching
引擎提供,但是有约束
小于900顶点的网格物体
如果着色器使用pos normal uv三种属性,则支持小于300顶点物体
如果使用pos normal uv0 uv1 切向量,只能处理小于180顶点
不要使用缩放
统一缩放的物体,不会与非统一缩放的物体进行批处理
1,1,1不与1,2,1处理,但是1,2,1与1,3,1同时处理
使用了不同材质,不进行
拥有ligthmap的物体含有额外隐藏的材质属性,如偏移和缩放系统数等,不会进行
多通道的shader会妨碍进行
纹理打包成图集来减少材质使用
减少反光,阴影等
物理设置合适的fixed timestep,不用使用mesh collider
gpu
性能瓶颈
填充率,可以简单的理解为图形处理单元每秒渲染的像素数量
像素的复杂度,比如动态阴影,光照,复杂的shader等等
几何体的复杂度(顶点数量)
当然还有GPU的显存带宽
瓶颈概括
顶点数量过多,像素计算过于复杂
GPU的显存带宽
优化目标
减少顶点数量,简化计算复杂度
材质的数目尽可能少,更容易batching
使用纹理图集,代替大量小贴图,可以更快加载,很少的状态转换
如果使用了纹理图集和共享材质,使用Renderer.sharedMaterial 来代替Renderer.material
使用光照纹理(lightmap)而非实时灯光
使用LOD,好处就是对那些离得远,看不清的物体的细节可以忽略
遮挡剔除(Occlusion culling)
使用mobile版的shader。简单
压缩图片,适应显存带宽
OpenGL ES 2.0使用ETC1格式压缩等等,在打包设置那里都有
使用MipMap
图例
可以根据实际情况,选择适合的小图来渲 染,虽然消耗一些内存,但是渲染质量会有提升,比压缩要好
内存
主要分为
Unity3D内部的内存
资源
纹理、网格、音频等
GameObject和各种组件
引擎内部逻辑需要的内存
如渲染器、物理系统、粒子系统等
Mono的托管内存
Mono的托管环境,满足跨平台的需要。就是传统的GC注意事项
各种类的实例,游戏引擎对各种控件的封装,引擎底部的各种控件,在C#层都有封装。堆上分配,就会涉及GC
引用的dll或者第三方dll所需要的内存
不太需要关注
企业微信截图_1575018537653