Unity性能优化-GC优化
Unity性能优化-GC优化
参考文献:https://unity3d.com/cn/learn/tutorials/topics/performance-optimization/optimizing-garbage-collection-unity-games
垃圾回收(GC)简介
在GC(Garbage Collection)过程中,垃圾回收器会检查堆(Heap)中的所有对象,搜索它们的引用,来判断这些对象是否还在作用域中。如果对象不在作用域中,它将被标记为需要删除。然后垃圾回收器会将这些被标记为需要删除的对象删除,收回它们所占用的内存空间。堆中的对象和代码中对象引用越多,垃圾回收过程中要进行的操作就越多,其开销也就越大。
堆内存空间不足、系统定时自动回收垃圾以及强制GC都会触发垃圾回收操作。频繁的进行堆内存分配和释放将会导致频繁的GC。在Unity中,只有 值类型局部变量 被分配在栈(Stack)中,它们不会引起GC,其他所有类型的数据都分配在堆中,由GC系统进行回收。
由GC引起的性能问题 主要表现 为:帧率低、性能时好时坏以及断断续续的出现卡顿。
降低由GC带来的影响
广泛的说,可以通过以下三种方式降低GC带来的影响:
- 减少GC运行所需的时间 —— 减少游戏中的堆内存分配和对象引用数目。
- 减少GC频率 —— 降低对内存的分配和释放频率。
- 在非性能瓶颈时期主动触发GC —— 尝试测算GC和堆空间扩张的时间来使其在可预测、适宜的时间发生。
减少垃圾的产生
- 使用缓存&不要在频繁调用的方法中分配堆内存。如果在需要频繁调用的方法中进行了堆内存分配,并且最后舍弃了这个新建的变量,这将会产生不必要的垃圾,应该考虑在方法外创建对该变量的引用,然后重复利用它。对于无法缓存的对象,应该尽量降低方法的执行次数,仅在需要时才去执行它。
- 使用清空集合替代新建集合。创建新的集合对象将会在堆空间分配内存。如果代码中多次创建新的集合,应该缓存对集合的引用,在下次需要使用新的集合时,将缓存的集合清空(Clear())而不是建立全新的集合。
- 使用对象池。如果游戏中需要频繁的创建再销毁某类对象,那么为该类对象建立对象池。
引起堆内存分配的常见原因
- 字符串(String/string)。C#中的字符串是引用类型而不是值类型,并且它是不可变的,每次对字符串的修改,实际上是创建了一个新的字符串对象。由于字符串在程序中非常常用,所以它可能积累出较多的垃圾。遵从下面的一些简单规则,可以保证由字符串所产生的垃圾最少:
- 减少不必要的字符串创建,将反复使用的字符串存储到变量中。
- 减少不必要的字符串修改,使用StringBuilder替换需要经常修改的字符串。
- 当Debug.Log()不再使用后立刻将其移除,每次打印日志至少会产生一个字符串。
- Unity方法调用。某些Unity方法的调用会进行堆内存分配,应该谨慎调用那些不是自己写的代码,无论它是来自Unity还是第三方插件。在Unity中,每次调用返回数组的方法或属性(访问器),都会创建一个新的数组再返回,字符串也是这样。减少此类调用或者使用替代方法,例如使用CompareTag()替换==操作。
- 装箱。将值类型变量当作引用类型变量使用时会进行自动装箱。在装箱过程中,Unity在堆内存中创建了一个临时的System.Object对象,使用后该对象作为垃圾被丢弃。装箱操作在代码中非常常见,容易积累垃圾。一个常见的会引起装箱的操作是将枚举类型作为Dictionary的键。
- 协程。调用StartCoroutine()方法会产生少量的垃圾,因为Unity需要创建一些类的实例用于管理协程。应该提前启动那些需要在高性能要求时期运行的协程,并小心处理可能引起延迟调用的嵌套协程。协程中的yield语句本身不需要进行堆内存分配,但由它所返回的值可能需要分配堆内存。一个常见的错误用法是在yield语句中使用new多次返回相同的值,例如
yield return new WaitForSeconds(1f);
,应该将WaitForSeconds(1f)对象缓存起来才对。 - foreach循环。在比Unity 5.5更早版本的Unity中,每次foreach循环都会产生垃圾,因为该语句引发了装箱操作。在Unity 5.5中,这个问题被修复了。如果没办法将Unity升级到5.5或更高版本,那么使用for或者while替代foreach。
- 方法引用。在Unity中,无论是对具名方法的引用还是对匿名方法的引用,都是引用类型变量,它们会引发堆内存分配。将匿名方法转换成闭包会明显的增加内存占用和堆内存分配次数。
- LINQ和正则表达式。LINQ和正则表达式都会因引发装箱操作而产生垃圾,最好不要使用它们。
优化代码结构使GC带来的影响最小化
不良的代码结构会增加垃圾回收器的工作量。比如,让垃圾回收器去检查原本不需要检查的对象。结构体(struct)是值类型的变量,但如果一个结构体中包含了引用类型的变量,那么垃圾回收器就必须去检查整个结构体。如果系统中有一个包含大量此类结构体的数组,那将会为垃圾回收器增加很多工作量。另一种情况就是 包含太多不必要的对象引用。当垃圾回收器为堆中的对象查找引用时,它必须在代码中查找当前对系统中的每个对象的引用。代码中对对象的引用越少,垃圾回收器要做的工作就越少。
手动进行强制GC
如果知道堆内存中有不再使用的已分配空间(例如加载资源所产生的垃圾)并且当前系统对性能的需求不苛刻(例如正在显示加载画面),则可以通过下面的代码强制垃圾回收器进行GC:
- 1