[转]Unity 2019:增量式GC

2020/09 27 17:09

为什么使用增量式垃圾回收

C#语言使用托管内存和自动垃圾回收,这意味着它使用自动化方法跟踪内存中的对象,然后释放不再使用对象的内存。

这种做法的优点是,开发者不必手动跟踪释放不需要的内存,因为垃圾回收器会自动执行此操作,这样会使开发者的工作更轻松,同时避免出现潜在Bug。缺点是垃圾回收器需要一些时间完成工作,而开发者或许不希望将特定时间用于此处。

Unity使用的是贝姆垃圾回收器(Boehm–Demers–Weiser garbage collector),该回收器利用了Stop-The-World机制,这意味着它会在执行垃圾回收时暂停运行中的程序,当完成垃圾回收后才恢复运行状态。

这可能会导致程序在运行过程中随时会出现延迟执行的现象,持续时间约在低于一毫秒到数百毫秒之间,具体取决于垃圾回收器需要处理的内存量和程序运行平台。

对于游戏这样的实时应用程序而言,这却是一个大问题,因为程序在运行过程会被垃圾回收器随时暂停的话,则无法维持流畅动画所需的稳定帧率。这类中断情况被称为GC峰值。通常开发者会通过编写代码来避免游戏运行期间创建“垃圾”内存,从而减少垃圾回收器的工作,但这种方法不总是简单可行。

如今Unity加入增量式垃圾回收功能,Unity仍使用同样的贝姆垃圾回收器,但它会以增量模式运行,从而将任务分解为多个部分。这样不必为了执行垃圾回收而长时间中断程序执行,而是用多个短时间的中断来完成。

虽然该方法从整体上不会让垃圾回收过程变快,但它能通过分配工作量到多个帧,显著减少GC峰值对动画流畅性的影响问题。

增量式垃圾回收测试

为了了解这种方法的影响,我们将查看Unity性能分析器中小型垃圾回收性能测试脚本的截图画面,该测试运行为macOS独立版本,分成未启用增量式垃圾回收和启用增量式垃圾回收二种情况。

查看垃圾回收性能测试脚本代码:https://files.unity3d.com/jonas/IncrementalGCTestScript.cs

垃圾回收性能测试脚本运行在60 fps状态,帧的蓝色部分是“脚本操作”,通过脚本的System.Threading.Thread.Sleep调用来模拟,黄色部分是Vsync,即等待下一帧开始,深绿色部分是垃圾回收。

下图是未启用增量式垃圾回收的情况,运行项目每几秒会出现约持续30毫秒的峰值,此时会中断60 fps的平稳帧率。

Unity 2019.1 Alpha新功能:增量式垃圾回收

下图是启用了增量式垃圾回收的情况,相同的项目维持在稳定的60 fps帧率,因为垃圾回收操作分到多个帧完成,只在这几帧使用了少量时间切片。

Unity 2019.1 Alpha新功能:增量式垃圾回收

下图展示了相同项目运行在启用增量式垃圾回收的状态,但这次每帧的“脚本操作”较少。同样地,垃圾回收操作被分为数帧完成。不同之处在于,这次垃圾回收每帧使用了更多时间,而且完成操作的总帧数较少。

这是因为我们根据使用Vsync或Application.targetFrameRate时的剩余可用帧时间,调整了分配给垃圾回收的时间。通过这种方法,我们使用等待时间来运行垃圾回收,从而使垃圾回收几乎不造成性能影响。

Unity 2019.1 Alpha新功能:增量式垃圾回收

如何启用增量式垃圾回收

目前Unity 2019.1 Alpha提供增量式垃圾回收功能,支持Mac、Windows、Linux、iOS、Android和Windows UWP平台,未来还会增加更多支持的平台。增量式垃圾回收需要使用新.NET 4.x Equivalent脚本运行时版本。

增量式垃圾回收目前是实验性选项,位于Player Settings窗口的“Other settings”部分,勾选Use incremental GC (Experimental)后,就可以构建播放器进行尝试。

通过使用Unity 2019.1加入的Scripting.GarbageCollector API,你可以更准确地控制增量式垃圾回收的行为。

Unity 2019.1 Alpha新功能:增量式垃圾回收

预期的一些问题

如果启用了增量式垃圾回收,垃圾回收器会将垃圾回收工作分为多次操作,并分配到多个帧完成。在出现GC峰值问题的大多数情况下,我们希望该功能可以减少问题的影响。但是Unity的开发内容非常丰富,有很多种处理方式,所以该功能很可能在某些情况下没有积极作用。

例如在增量式垃圾回收分解任务时,分解的部分是标记阶段,此时它会扫描所有托管对象,寻找对象引用的其它对象,从而跟踪仍在使用的对象。这个过程会假设对象间的多数引用不会在任务分解时改变。

当引用改变时,修改的对象需要在下一次迭代过程重新扫描。该过程可能会造成增量式回收无法完成,因为它总会增加更多任务。这种情况下,垃圾回收会回退为完整的非增量式回收,创建不断变化引用的人工测试用例很简单,这种情况下增量式垃圾回收会比非增量式垃圾回收的效果更差。

此外,在使用增量式垃圾回收器时,Unity需要生成额外代码,用于在引用变化时通知垃圾回收器,从而使垃圾回收器知道是否需要重新扫描对象。在引用变化时,该做法会增加性能开销,在部分托管代码中造成显著的性能影响。

然而,我们相信多数常见Unity项目会从增量式垃圾回收受益,特别是受到垃圾回收影响较大的项目。

实验性阶段

在Unity 2019.1中,增量式垃圾回收目前是实验性预览功能,有以下几点原因。

  •  功能仍未支持所有平台,我们希望支持更多的平台。

  •  如“预期的一些问题”部分中所谈的,我们希望增量式垃圾回收能对大多数Unity开发内容有益,或者至少不会对性能产生不利影响,我们测试的多个项目都实现了预期效果。但由于Unity开发内容非常多样化,我们希望确保这种假设适用于庞大的Unity生态系统。

  •  为了通知垃圾回收器托管内存中引用的变化,在添加写入屏障时,对于Unity代码和脚本虚拟机(Mono,IL2CPP)的要求可能会产生我们无法注意到的潜在Bug,进而导致仍要使用的对象被垃圾回收功能处理掉。

现在我们完成了大量的测试,还没发现其它相关问题,所以我们认为该功能比较稳定。但是由于Unity开发内容的多样性,而且在实践中可能很难触发这类Bug,所以我们不能完全排除存在问题的可能性。

所以,我们相信该功能总体上达到了预期效果,但是Unity生态系统非常复杂,我们需要更多的时间曝露问题,并根据开发者的反馈结果来移除它的实验性标签。

未来计划

2019年,增量式垃圾回收功能计划部分改动:
  •  为其它平台添加支持
  •  移除实验性标签
  •  添加对在增量式垃圾回收模式下运行编辑器的支持
  •  使增量式垃圾回收成为默认选项

不使用其它垃圾回收器的原因

在讨论增量式垃圾回收时,我们经常被问到为什么不使用其它垃圾回收解决方案,例如Xamarin的Sgen垃圾回收器。

实际上我们考虑过其它选择,包括自己编写垃圾回收器。但目前使用贝姆垃圾回收器并转为增量模式的原因是,这是获得显著改进的同时,我们能够实现的最安全的做法。

就像在实验性阶段部分所说,其中存在的一个风险是在写入屏障丢失或位置错误时产生Bug。但如今,很多比Unity所用方法更新的垃圾回收解决方案都有添加写入屏障的要求。所以通过使用贝姆垃圾回收器,我们可以从改变垃圾回收器的风险中隔离出添加写入屏障产生的风险。

我们会继续关注相关开发进展,并了解加入增量式贝姆垃圾回收器后,用户的需求变化。如果我们发现增量式贝姆垃圾回收器仍会让很多用户为解决GC峰值或其它问题所苦恼,我们会考虑使用其它解决方案。

小结

开发者的反馈非常重要,如果你正在使用Unity 2019.1 Alpha,欢迎将你的反馈通过开发论坛让我们知道。

反馈增量式垃圾回收:https://forum.unity.com/threads/incremental-gc-feedback-thread.588664/