使用Unity构建游戏时,我应该如何考虑GC?


15

*据我所知,iOS版Unity3D基于Mono运行时,而Mono仅具有世代标记和清除GC。

该GC系统无法避免GC时间停止游戏系统。实例池可以减少这种情况,但不能完全减少,因为我们无法控制实例化发生在CLR的基类库中。那些隐藏的小型且频繁的实例最终将增加不确定的GC时间。定期强制使用完整的GC将大大降低性能(实际上,Mono可以强制使用完整的GC吗?)

那么,如何在使用Unity3D时避免GC时间又不会大幅降低性能呢?


3
最新的Unity Pro Profiler包括某些函数调用中产生了多少垃圾。您可以使用此工具来帮助查看可能正在生成垃圾的其他地方,否则这些垃圾可能不会立即显而易见。
Tetrad 2012年

Answers:


18

幸运的是,正如您所指出的,COMPACT Mono版本使用了世代的GC(与Microsoft的WinMo / WinPhone / XBox完全相反,后者仅维护一个固定列表)。

如果您的游戏很简单,GC应该可以很好地处理它,但是您可能需要研究以下几点。

过早优化

首先,请先确定这实际上是您遇到的问题,然后再尝试修复它。

合并昂贵的参考类型

您应该合并经常创建或具有深层结构的引用类型。每个示例如下:

  • 经常创建:子弹游戏中的Bullet对象。
  • 深度结构:用于AI实现的决策树。

您应该使用a Stack作为池(与大多数使用a的实现不同Queue)。这样做的原因是,Stack如果使用a,则将对象返回到池中,然后其他对象立即将其抓住;如果幸运的话,它更有可能进入活动页面,甚至进入CPU缓存。只是快一点。此外,请始终限制池的大小(如果超出限制,则不理会“ checkins”)。

避免创建新列表来清除它们

List当您真正想要一个新东西时,不要创建Clear()它。您可以重新使用后端阵列并保存大量的阵列分配和副本。与此尝试类似,创建具有有意义的初始容量的列表(请记住,这不是限制-仅仅是起始容量),它不需要准确,而只是一个估计值。这基本上适用于任何集合类型-除了LinkedList

尽可能使用结构数组(或列表)

如果在对象之间传递结构,则使用结构(或通常是值类型)几乎不会带来任何好处。例如,在大多数“好”粒子系统中,单个粒子存储在一个巨大的数组中:数组和索引被传递,而不是粒子本身传递。之所以如此有效,是因为当GC需要收集数组时,它可以完全跳过内容(这是原始数组-在此无需执行任何操作)。因此,GC无需查看1万个对象,而只需查看1个数组:巨大的收益!同样,这仅适用于值类型

在RoyT之后。提供了一些可行和建设性的反馈,我觉得我需要对此进行更多的扩展。仅在处理大量实体(成千上万)时,才应使用此技术。另外,它必须是一个结构,该结构必须没有任何引用类型字段,并且必须位于显式类型的数组中。与他的反馈相反,我们将其放置在一个数组中,该数组很可能是类中的一个字段-意味着它将要堆积在堆上(我们不打算避免堆分配-只是避免GC工作)。我们真正关心的是它是一个连续的内存块,具有很多值,GC可以在O(1)操作而不是O(n)操作中简单地查看它们。

您还应该尽可能靠近应用程序启动位置分配这些数组,以减少发生碎片的可能性,或者减少GC尝试移动这些块时的工作量(并考虑使用混合链接列表而不是内置List类型) )。

GC.Collect()

这绝对是使用世代GC 射击自己的最佳方法(请参阅:“性能注意事项”)。您应该只把它当你已经创建了一个极端的垃圾量-在那里,可能是一个问题和一个实例是已加载的水平含量刚过-甚至那么你或许应该只收集第一代()希望防止将对象推广到第三代。GC.Collect(0);

IDisposable和字段清零

当您不再需要对象时(对于受约束的对象更是如此),将字段为空是值得的。原因在于GC的工作原理的详细信息:它仅删除未植根(即被引用)的对象,即使该对象由于当前集合中其他对象被删除而已被取消植根(注意:这取决于GC)使用中的风味-有些实际上可以清除链条)。此外,如果某个对象在集合中幸存下来,则会立即提升到下一代-这意味着在字段中放置的任何对象都会在集合期间得到提升。收集的每一代都成倍增加(并且很少发生)。

请看以下示例:

MyObject (G1) -> MyNestedObject (G1) -> MyFurtherNestedObject (G1)
// G1 Collection
MyNestObject (G2) -> MyFurtherNestedObject (G2)
// G2 Collection
MyFurtherNestedObject (G3)

如果MyFurtherNestedObject包含一个兆字节的对象,则可以保证GC不会长时间查看它-因为您无意中将其升级到了G3。与此示例进行对比:

MyObject (G1) -> MyNestedObject (G1) -> MyFurtherNestedObject (G1)
// Dispose
MyObject (G1)
MyNestedObject (G1)
MyFurtherNestedObject (G1)
// G1 Collection

Disposer模式可帮助您设置一种可预测的方式来请求对象清除其私有字段。例如:

public class MyClass : IDisposable
{
    private MyNestedType _nested;

    // A finalizer is only needed IF YOU CONTROL UNMANAGED RESOURCES
    // ~MyClass() { }

    public void Dispose()
    {
       _nested = null;
    }
}

1
WTF您即将开始吗?Windows上的.NET Framework绝对是世代相传的。他们的类甚至具有GetGeneration方法。
DeadMG 2012年

2
Windows上的.NET使用了世代垃圾收集器,但是WP7和Xbox360上使用的.NET Compact Framework的GC更为有限,尽管它可能不只是一个完整的清单,也不是一个完整的世代垃圾收集器。
罗伊(Roy T.)

乔纳森·迪金森(Jonathan Dickinson):我想补充一下,结构并不总是答案。在.Net中,也可能在Mono中,一个结构不必一定存在于堆栈中(这很好),但也可以存在于堆中(这是您需要垃圾收集器的地方)。规则很复杂,但通常在结构包含非值类型时会发生。
罗伊(Roy T.)2012年

1
@RoyT。见上面的评论。我指出了结构对于阵列中大量数据的线性排列(例如粒子系统)非常有用。如果您担心数组中的GC结构,那该怎么办?用于粒子之类的数据。
乔纳森·迪金森

10
@DeadMG还强烈不建议使用WTF-考虑到您实际上没有正确阅读答案。在这样的行为举止普遍良好的社区中,发表令人讨厌的评论之前,请保持冷静并重新阅读答案。
乔纳森·迪金森

7

除非您调用需要它的东西,否则很少有动态实例化会自动在基础库中发生。绝大多数的内存分配和释放都来自您自己的代码,您可以控制它们。

据我了解,您不能完全避免这种情况-您所要做的就是确保在可能的情况下回收和缓冲对象,使用结构代替类,避免使用UnityGUI系统,并且通常避免在此期间在动态内存中创建新对象当性能很重要时。

您还可以在某些时间强制进行垃圾收集,这可能有帮助也可能没有帮助:请参阅此处的一些建议:http : //unity3d.com/support/documentation/Manual/iphone-Optimizing-Scripts.html


2
您应该真正解释强制GC的含义。如果使用不当,则会破坏GC的启发式方法和布局,并可能导致更大的内存问题:XNA / MVP /工作人员社区通常认为加载后内容是强制收集的唯一合理时间。如果您仅对此事奉献一句话,则应该避免完全提及它。GC.Collect()=现在可以释放内存,但是以后很可能会出现问题。
乔纳森·迪金森

不,您应该解释强制使用GC的含义,因为您比我了解的更多。:)但是,请记住,Mono GC不一定与Microsoft GC相同,并且风险可能也不相同。
Kylotan'3

不过,+ 1。我继续讲解了一些有关GC的个人经验-您可能会觉得有趣。
乔纳森·迪金森
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.