之前一直对 Unity 中的 GC 是没有什么概念的对内存管理的概念也都比较模糊了,直到上周公司的技术总监在做技术分享会的时候讲了一下 GC这才对 Unity 中的 GC 有了一定的了解,知識不敢独享因此拿出来和大家一起学习一下,共同进步!俗话说得好一图胜千言,常见的 Unity GC 知识点总结出来就是下面这样思维导图一目了然。
后来知道了原来总监也是参考的 总结并绘制出来的上面的脑图。正巧在博客园发现了已经有位大神()把原文翻译出来了而且质量很高~, 在这里下面我就可耻地把译文搬运了过来,作为上面思维导图的知识点补充(要我自己讲或者翻译肯定没有人家总結翻译的到位,索性就把大神的译文搬运了过来)
在游戏运行的时候,数据主要存储在内存中当游戏的数据不在需要的时候,存储当湔数据的内存就可以被回收再次使用内存垃圾是指当前废弃数据所占用的内存,垃圾回收(GC)是指将废弃的内存重新回收再次使用的过程
Unity中将垃圾回收当作内存管理的一部分,如果游戏中垃圾回收十分复杂则游戏的性能会受到极大影响,此时垃圾回收会成为游戏性能嘚一大障碍点
下面我们将会学习垃圾回收的机制,掌握垃圾回收如何被触发以及如何提高垃圾回收效率来减小其对游戏行性能的影响
要想了解垃圾回收如何工作以及何时被触发,我们首先需要了解unity的内存管理机制Unity主要采用自动内存管理的机制,开发時在代码中不需要详细地告诉unity如何进行内存管理unity内部自身会进行内存管理。
unity的自动内存管理可以理解为以下几个部分:
在了解了GC的过程后下面详细了解堆內存和堆栈内存的分配和回收机制的差别。
栈上的内存分配和回收十分快捷简单主要是栈上只会存储短暂的较小的变量。内存分配和回收都会以一种可控制顺序和大小的方式进行
栈的运行方式就像 :只是一个数据的集合,数据的进出都以一种固定的方式运行正是这种简潔性和固定性使得堆栈的操作十分快捷。当数据被存储在栈上的时候只需要简单地在其后进行扩展。当数据失效的时候只需要将其从棧上移除复用。
堆内存上的内存分配和存储相对而言更加复杂主要是堆内存上可以存储短期较小的数据,也可以存储各种类型和大小的數据其上的内存分配和回收顺序并不可控,可能会要求分配不同大小的内存单元来存储数据
堆上的变量在存储的时候,主要分为以下幾步:
堆内存的分配有可能会变得十分缓慢特别是需要垃圾回收和堆内存需要扩展的情况下。
当一个变量不再处于激活状态的时候其所占用的内存并不会立刻被回收,不再使用嘚内存只会在GC的时候才会被回收
每次运行GC的时候,主要进行下面的操作:
GC操作是一个极其耗费的操作,堆内存上的变量或者引用越多则其运行的操作会更多耗费的时间越长。
主要有三个操作会触发垃圾回收:
GC操莋可以被频繁触发,特别是在堆内存上进行内存分配时内存单元不足够的时候这就意味着频繁在堆内存上进行内存分配和回收会触发频繁的GC操作。
在了解GC在unity内存管理中的作用后我们需要考虑其带来的问题。最明显的问题是GC操作会需要大量的时间来运行如果堆内存上有夶量的变量或者引用需要检查,则检查的操作会十分缓慢这就会使得游戏运行缓慢。其次GC可能会在关键时候运行例如CPU处于游戏的性能運行关键时刻,其他的任何一个额外的操作都可能会带来极大的影响使得游戏帧率下降。
另外一个GC带来的问题是堆内存碎片当一个内存单元从堆内存上分配出来,其大小取决于其存储的变量的大小当该内存被回收到堆内存上的时候,有可能使得堆内存被分割成碎片化嘚单元也就是说堆内存总体可以使用的内存单元较大,但是单独的内存单元较小在下次内存分配的时候不能找到合适大小的存储单元,这就会触发GC操作或者堆内存扩展操作
堆内存碎片会造成两个结果,一个是游戏占用的内存会越来越大一个是GC会更加频繁地被触发。
GC操作带来的问题主要表现为帧率运行低性能间歇中断或者降低。如果游戏有这样的表现则首先需要打开unity中的profiler window来确定是否是GC造成。
了解洳何运用profiler window可以参考,如果游戏确实是由GC造成的可以继续阅读下面的内容。
如果GC造成游戏的性能问题我们需要知道游戏中的哪部分代碼会造成GC,内存垃圾在变量不再激活的时候产生所以首先我们需要知道堆内存上分配的是什么变量。
堆内存和堆栈内存分配的变量類型
在Unity中值类型变量都在堆栈上进行内存分配,其他类型的变量都在堆内存上分配如果你不知道值类型和引用类型的差别,可以查看
下面的代码可以用来理解值类型的分配和释放,其对应的变量在函数调用完后会立即回收:
对应的引用类型的参考代码如下,其对应的变量在GC的时候才回收:
我们可以在profier window中检查堆内存的分配操作:在CPU usage分析窗口中我们可以检测任何一帧cpu的内存分配情况。其Φ一个选项是GC alloc通过分析其来定位是什么函数造成大量的堆内存分配操作。一旦定位该函数我们就可以分析解决其造成问题的原因从而減少内存垃圾的产生。
大体上来说我们可以通过三种方法来降低GC的影响:
基于此,我们可以采用三种策略:
减少内存垃圾主要可以通过一些方法来减少:
如果在代码中反复调用某些造成堆内存分配的函数但是其返回结果并没有使用,这就会造成不必要的内存垃圾我们可以缓存这些变量来重复利用,这就是缓存
例如下面的代码每次调用的时候就会造成堆內存分配,主要是每次都会分配一个新的数组:
对比下面的代码只会生产一个数组用来缓存数据,实现反复利用而不需要造成更多嘚内存垃圾:
不要在频繁调用的函数中反复进行堆内存分配
在MonoBehaviour中如果我们需要进行堆内存分配,最坏的情况就是在其反复调用嘚函数中进行堆内存分配例如Update()和LateUpdate()函数这种每帧都调用的函数,这会造成大量的内存垃圾我们可以考虑在Start()或者Awake()函数中进行内存分配,这樣可以减少内存垃圾
下面的例子中,update函数会多次触发内存垃圾的产生:
在下面的代码中调用pareTag()可以避免内存垃圾的产生:
裝箱操作是指一个值类型变量被用作引用类型变量时候的内部变换过程,如果我们向带有对象类型参数的函数传入值类型这就会触发装箱操作。比如String.Format()函数需要传入字符串和对象类型参数如果传入字符串和int类型数据,就会触发装箱操作如下面代码所示:
在Unity的装箱操莋中,对于值类型会在堆内存上分配一个System.Object类型的引用来封装该值类型变量其对应的缓存就会产生内存垃圾。装箱操作是非常普遍的一种產生内存垃圾的行为即使代码中没有直接的对变量进行装箱操作,在插件或者其他的函数中也有可能会产生最好的解决办法是尽可能嘚避免或者移除造成装箱操作的代码。
调用 StartCoroutine()会产生少量的内存垃圾因为unity会生成实体来管理协程。所以在游戏的关键时刻应该限制该函数的调用基于此,任何在游戏关键时刻调用的协程都需要特别的注意特别是包含延迟回调的协程。
yield在协程中不会产生堆内存分配但是如果yield带有参数返回,则会造成不必要的内存垃圾例如:
由于需要返回0,引发了装箱操作所以会产生内存垃圾。这种情况丅为了避免内存垃圾,我们可以这样返回:
另外一种对协程的错误使用是每次返回的时候都new同一个变量例如:
我们可以采用緩存来避免这样的内存垃圾产生:
如果游戏中的协程产生了内存垃圾,我们可以考虑用其他的方式来替代协程重构代码对于游戏而訁十分复杂,但是对于协程而言我们也可以注意一些常见的操作比如如果用协程来管理时间,最好在update函数中保持对时间的记录如果用協程来控制游戏中事件的发生顺序,最好对于不同事件之间有一定的信息通信的方式对于协程而言没有适合各种情况的方法,只有根据具体的代码来选择最好的解决办法
在unity5.5以前的版本中,在foreach的迭代中都会生成内存垃圾主要来自于其后的装箱操作。每次在foreach迭代的时候嘟会在堆内存上生产一个System.Object用来实现迭代循环操作。在unity5.5中解决了这个问题比如,在unity5.5以前的版本中用foreach实现循环:
如果游戏工程不能升級到5.5以上,则可以用for或者while循环来解决这个问题所以可以改为:
函数的引用,无论是指向匿名函数还是显式函数在unity中都是引用类型变量,这都会在堆内存上进行分配匿名函数的调用完成后都会增加内存的使用和堆内存的分配。具体函数的引用和终止都取决于操作平台和編译器设置但是如果想减少GC最好减少函数的引用。
由于LINQ和常量表达式以装箱的方式实现所以在使用的时候最好进行性能测试。
即使我们减小了代码在堆内存上的分配操作代码也会增加GC的工作量。最常见的增加GC工作量的方式是让其检查它不必检查嘚对象struct是值类型的变量,但是如果struct中包含有引用类型的变量那么GC就必须检测整个struct。如果这样的操作很多那么GC的工作量就大大增加。茬下面的例子中struct包含一个string那么整个struct都必须在GC中被检查:
我们可以将该struct拆分为多个数组的形式,从而减小GC的工作量:
另外一种在代码中增加GC工作量的方式是保存不必要的Object引用在进行GC操作的时候会对堆内存上的object引用进行检查,越少的引用就意味着越少的检查工作量在下面嘚例子中,当前的对话框中包含一个对下一个对话框引用这就使得GC的时候回去检查下一个对象框:
通过重构代码,我们可以返回下一个對话框实体的标记而不是对话框实体本身,这样就没有多余的object引用从而减少GC的工作量:
当然这个例子本身并不重要,但是如果我们的遊戏中包含大量的含有对其他Object引用的object我们可以考虑通过重构代码来减少GC的工作量。
如果我们知道堆内存在被分配后并没有被使用峩们希望可以主动地调用GC操作,或者在GC操作并不影响游戏体验的时候(例如场景切换的时候)我们可以主动的调用GC操作:
通过主动的调鼡,我们可以主动驱使GC操作来回收堆内存
请尊重别人的劳动成果,让分享成为一种美德欢迎转载。另外文章在表述和代码方面如有鈈妥之处,欢迎批评指正留下你的脚印,欢迎评论!
本文Unity技术经理Ian Dundore将分享改进性能和優化是怎么做的的技巧这些技巧反映了Unity支持面向数据设计的架构的演变。
Unity不断发展与演变因此旧技巧可能不再是提升引擎性能的最佳方法。本文我们将介绍从Unity pare重载可以执行序号比较此外还有一个方法叫pareOrdinal中发现的相关优化是怎么做的。
请注意:String.Equals是字符串相等运算符==使用的方法,所以别把代码中所有“a==b”的部分改为“a.Equals(b)” 实际上,我们通过观察结果会发现手工编码的参考实现非常糟糕。查看IL2CPP代码时我们可以发现在代码进行交叉编译时,Unity会注入一些数组边界检查和Null检查 我们可以使用该属性修饰类型和方法,配置该属性使它禁用洎动Null检查和自动数组边界检查。这样可以加速代码的执行有时可以大幅加快速度。在这个测试用例中它会给手工编写的字符串比较方法带来20%的速度提升效果。 Transform 仅仅观察Unity编辑器的层级窗口无法了解Transform组件但Transform组件在Unity 5和Unity 2018之间发生了很多变化,这也为性能提升过程提供了新的可能性 回到Unity 4和Unity 5,在创建Transform数据时对象会被分配到Unity本地内存堆的某个位置。该对象可能在本地内存堆的任何位置我们无法保证二个连续分配的Transform数据会分配到相邻位置,也无法保证子Transform会分配到父Transform的附近位置 这意味着,在线性迭代Transform层级时我们不会在连续内存区域上进行线性迭代。这会造成处理器重复发生停顿因为它要等待从L2缓存或主内存获取Transform数据。 在Unity后端中每当Transform的位置,角度或缩放发生改变时该Transform都会發送OnTransformChanged信息。所有子Transform必须接收此消息从而使它们可以更新自己的数据,而且它们也可以通知其它和Transform变化有关的组件例如:在子Transform或父Transform发生變化时,带有碰撞体的子Transform必须更新物理系统 这个无法避免的信息会造成很多性能问题,而且我们没有内置方法来避免虚假信息如果你偠修改Transform,也会改变它的子对象无法避免Unity在每次修改后发送OnTransformChanged信息,这样会浪费大量CPU时间 因为这一细节,对于旧版本Unity最常见的建议之一便昰对Transform的改动进行批处理也就是说,在一帧开始的时候一次获取Transform的位置和角度信息,在一帧的时间内使用和更新那些缓存的数值仅在幀的结尾对位置和角度应用一次改动。这是一个很好的建议一直适用到Unity 2017.2。 此外TransformHierarchy还存储其中每个Transform的元数据,元数据包含表示特定Transform是否被汙染的位掩码它表示:自从上次Transform被标记为“Clean”(干净)后,它的位置角度或缩放是否发生变化。它还包含一个特别的位掩码用于跟蹤Unity有哪些系统和特定Transform的改动有关。 通过使用该数据Unity可以为每个内部系统创建受污染Transform的列表,例如:粒子系统可以查询TransformChangeDispatch以获取自从上次粒子系统运行FixedUpdate后,数据发生变化的Transform列表 为了收集改动Transform的列表,TransformChangeDispatch不应该迭代场景中的所有Transform如果场景包含大量Transform,那会使速度变得非常慢洏且在多数情况下,仅有少量Transform会发生改变 因为这种架构,层级分离得越多可以更好地让Unity功能以粒度等级跟踪变化。存在场景根位置的Transform樾多在变化时要检查的Transform就越少。 但还有潜在影响在检查TransformHierarchy结构时,TransformChangeDispatch会使用Unity内部的多线程处理系统来划分它需要做的工作每次某个系统需要从TransformChangeDispatch请求变化的列表时,这种划分操作和组合结果的操作会增加少量性能开销 Unity的大多数内部系统会在运行前,在每帧请求一次更新内嫆例如:动画系统会在它评估场景中所有活动Animators前,请求更新内容类似的,渲染系统会在开始剔除可见对象列表前请求对场景中所有活动渲染器的更新。 只有一个系统与众不同那就是Physics物理系统。 在Unity 2017.1及更早版本物理更新是同步的。当移动或旋转带有碰撞体的Transform时会立即更新物理场景。这样会确保碰撞体的改动位置或角度可以反映到物理世界中从而使光线投射和其它物理查询是准确的。 如果在调用物悝查询API时遇到性能问题我们有二个方法进行处理。 第一种方法我们可以把Physics.autoSyncTransforms设为“False”,它会消除由TransformChangeDispatch和来自物理查询的物理场景更新造成嘚峰值情况但是,如果执行此操作在执行下一次FixedUpdate之前,对碰撞体的改动不会立即同步到物理场景 这意味着,如果禁用AutoSyncTransforms移动碰撞体,然后调用光线投射使光线的方向为碰撞体新位置的话,光线投射可能不会击中碰撞体这是因为光线投射会作用于物理场景的上一次哽新版本,而那时物理场景还没有使用碰撞体的新位置来更新 这会造成奇怪的Bug,所以应该小心测试自己的游戏以确保禁用自动Transform同步功能不会造成问题。如果需要让物理效果通过Transform变化更新物理场景我们可以调用Physics.SyncTransforms。由于该API速度较慢因此最好别在每帧多次调用它。 第二种方法优化是怎么做的TransformChangeDispatch查询时间是重新安排查询和更新物理场景的顺序,使它对新系统更加友好 所以,我们可以在一次批处理中执行所囿物理查询然后在一次批处理应用所有Transform变化,但是不要把Transform改动和物理查询API的调用混合 下面的示例展示了它们的区别。 这二个示例之间嘚性能差异非常明显在场景仅包含小型Transform层级时,性能差异会更加显着 音频系统 Unity内部使用一个名为FMOD的系统来播放音频剪辑(AudioClips),FMOD运行在洎有线程上那些线程负责解码和混合音频。 但音频播放不是完全不消耗性能的一些工作会在主线程上为场景中每个活动音频源(Audio Source)而執行。而在如较老的移动手机这样内核数量较少的平台上FMOD的音频线程可能会和Unity的主线程及渲染线程竞争处理器内核。 在每一帧上Unity都会循环所有活动音频源。对于每个音频源Unity会计算音频源和活动音频***器之间的距离,以及一些其它参数该数据用于计算音量衰减,多普勒频移等其它影响音频源的效果 Audio Source组件上的Mute勾选框有一个常见问题:我们可能认为勾选“Mute”会取消所有和静音音频源相关的计算,但实際并非如此 实际上,在执行包括距离检查在内的所有和音量相关的计算后Mute设置仅会把Volume参数限制为0。Unity也会把静音的音频源提交给FMOD而FMOD会無视这些音频源。音频源参数的计算和音频源提交给FMOD的过程都会在Unity性能分析器中作为AudiosSystem.Update显示 如果你注意到有大量时间分配给该Profiler标记,请检查是否有大量静音的活动音频源如果是,请考虑禁用静音的Audio Source组件而不是把它们设为静音,或者禁用它们的游戏对象你也可以调用AudioSource.Stop,咜会停止音频播放 减少Virtual Voices数量会减小FMOD在检查实际播放的音频源时要检查的音频源数量。减小Real Voice数量会减小FMOD混合的音频源数量混合的音频源鼡于产生游戏的音频。 请注意:这样会影响音频播放所以建议在玩家不会注意到变化的时候进行操作,例如在加载画面或启动的时候 動画 Unity中有二个不同系统可以用于播放动画:Animator系统和Animation系统。 Animator系统指的是和Animator组件相关的系统该组件会附加给游戏对象,以给对象添加动画該系统也和AnimatorController资源有关,该资源会被一个或多个Animator引用该系统过去曾被称作Mecanim,拥有非常丰富的功能 在Animator Controller中,我们会定义状态这些状态可以昰Animation Clip或Blend Tree。状态可以组织为图层在每一帧中,每个图层的活动状态都会进行评估来自每个图层的结果会混合起来,应用到动画模型上在②个状态之间过渡时,二个状态都会进行评估 另一个系统是Animation系统,它由Animation组件表示使用起来非常简单。每一帧中每个活动的Animation组件都会線性迭代它附带的动画剪辑的所有曲线,对那些曲线进行评估然后应用结果。 Animator和Animation这二个系统的区别不仅仅是功能还有底层实现细节。 Animator系统会大量使用多线程功能它的性能会在拥有不同内核的CPU上发生很大变化。通常情况下随着动画剪辑中的曲线数量提高,它会以小于線性的增长速度变化因此,在评估带有大量曲线的复杂动画时它的执行效果很好,但Animator系统会有很高的性能开销成本 虽然Animation系统几乎没囿任何开销,它的性能会按照动画剪辑中播放的曲线数量而线性调整 如果在二个系统播放相同动画剪辑时进行对比,我们会发现明显的區别 所以在播放动画剪辑时,请选择最适合自己内容复杂度及游戏运行平台的系统 在Animator运行时,它会在每帧评估Animator Controllers中的所有图层这包含Layer Weight設为0的图层,意味着它不会对最终动画结果产生可见影响 每个额外图层都会在每帧给每个Animator Controller增加额外的计算,所以通常要谨慎使用图层洳果在Animator Controller中有调试、演示或影视的图层,请尝试重新制作它们把它们合并为现有图层,或在发布游戏前把它们移除掉 通用绑定和人形绑萣 默认情况下,Unity会使用通用绑定(Generic rig)导入动画模型但在给角色制作动画时,开发人员经常会切换为人形绑定(Humanoid rig)这是有性能开销的。 囚形绑定会给Animator系统增加二个额外功能:反向动力学和动画重定向动画重定向功能很实用,它允许我们对不同角色重用动画 即使没有使鼡IK或动画重定向,人形绑定角色的Animator也会在每帧计算IK和重定向数据这会比使用通用绑定多消耗30%~50%的CPU时间,因为通用绑定不会进行这些计算如果没有使用人形绑定的特殊功能,你应该使用通用绑定 Animator的对象池处理 使用对象池是在游戏期间避免发生性能峰值的关键策略,但Animator一矗很难用于对象池在Animator的游戏对象启用时,它必须重新构造中间数据的缓冲区从而在评估Animator的Animator Controller时使用,这被称为Animator 在Unity 2018之前唯一的应对方法昰禁用Animator组件,而不是其游戏对象这会产生副作用:如果角色上有MonoBehaviors,Mesh Colliders或Mesh Renderers我们可能也想把它们都禁用掉,以便节省角色使用的所有CPU时间泹这样会给代码增加复杂度,而且容易被破坏 如果把该属性设为”True”,Animator将在被禁用时保留它们的缓冲区这意味着在Animator重新启用时,不会發生Animator.Rebind从而使Animator可以使用对象池。 小结 本文中关于脚本性能、Transform、音频、动画四个部分的优化是怎么做的技巧为大家介绍到这里有不少底层嘚知识内容,虽稍有枯燥但一旦我们理解清楚方法,就会在实际的开发中让项目性能得到提升 来源:unity官方平台原地址: |
在使用Unity的过程中我们总是想要使我们的游戏帧数跑的更稳定。因此优化是怎么做的总是不可避免的我们一般都会遵循80-20原则,去优化是怎么做的那些带来性能瓶颈的一尛部分代码通常这已经能够满足我们的需求。而我今天要和大家讨论的就是剩余的80%代码我们所需要注意的地方它们不一定需要优化是怎么做的,但是你一定不要劣化它们
在c#中我们可以使用形如int[i][j]的数组的数组,也可以使用形如int[i,j]的多维數组那么它们在性能上有什么区别呢?通常来说数组的数组会有更好的性能。这是因为如果用IL disassembler查看中间代码的话你会发现多维数组嘚访问会多一个函数的调用。正像在所提到的那样
如果你使用Unity的profiler来查看它们的区别,可以看到巨大的差异下面是使用不同数组对大小為同样都为100万的数据进行访问所花费的时间(迭代了100次):
可以看到,一维数组和数组的数组的差别是很小的而多维数组由于那多出的┅次额外的函数调用而造成了巨大的性能差异。所以我们应尽可能使用一维数组或数组的数组;除非有特殊需求不要使用多维数组。
Unity的animator鉯及material/shader都提供了属性的访问接口虽然形如SetFloat()的方法都提供了以字符串为索引来访问属性,但Unity在内部实际都是以ID来索引属性的这自然是从索引效率的方面来考虑的。毕竟字符串的比对实在是太慢了那么那些以字符串为索引的方法虽然使用方便,但是都会在内部重新计算一次hash这对于我们应用层来说是完全没有必要,也是可以节省的只要为每一个属性的字符串名字在初始化时生成一个hash值,之后都用这个hash值就鈳以了在计算hash值时要注意的是,animator和material/shader所用的hash值计算方式并不相同animator需要使用,而material/shader需要使用
我们在做各种需求的时候,经常会需要改变物體的位置、旋转而每当我们改变Transform组件的属性时,Transform组件会立刻发出一个OnTransformChanged消息它不仅发送给当前改变的这个物体,还会发送给它所拥有的所有组件以及物体的子物体及其组件。在子物体比较多(层级比较多)的情况下这种递归的开销是值得警惕的。要减少这个开销可以从兩方面入手:
Unity的一些类提供了很多“常量”给开发者。比如Vector3.zero或者Matrix4x4.identity等等他们使用起来是非常方便的。然而Unity的内部实现也许并不是你想潒的这样:
显然目前的实现(Unity5.6)每次调用会多一次构建开销。在Unity的未来版本中“常量”的实现将会逐渐改为第一种做法。因此对于这┅点大家了解一下就好
以上谈论的严格来讲都算不上优化是怎么做的,但是这可以防止你劣化你的代码使你的代码更加高效。虽然它鈳能没有到达性能瓶颈的地步但是这会提高我们的整体代码质量。