跳转至

优化迭代和协程的 GC

主要涉及 foreach、yield、Coroutine 的 GC 优化。不对 Unity 2022 LTS 以下的版本负责。

for 和 foreach

对数组来说,foreach 会被编译器转成 for,两者没什么区别。对于其他集合,foreach 的本质就是 GetEnumerator() 获取迭代器,然后不断 MoveNext() 并获取 Current

用 foreach 前,先检查一下 GetEnumerator() 的返回值类型。如果迭代器本身不是值类型,或者返回时会装箱的话,每次用 foreach 都会创建一个新的迭代器对象,产生 GC Alloc。这种情况尽量用 for。

像平时常用的 List<T>Dictionary<TKey, TValue> 的迭代器都是值类型的,可以放心用 foreach。

不滥用 yield

在 C# 中,用 yield 语句可以快速实现 IEnumerator<T>IEnumerable<T>。但是,这种方式生成的迭代器、集合都是引用类型的,对 foreach 不友好。

就算编译器生成值类型迭代器,因为方法返回的类型是 IEnumerator<T>,所以返回时也会装箱,还不如直接生成为引用类型。

如果迭代器比较常用的话,还是应该自己手写一个值类型的版本,并且声明一个返回该类型迭代器的 GetEnumerator() 方法(扩展方法也行)。

协程和 UniTask

在 Unity 里,协程本质就是个迭代器,用 yield 语句来编写。每次开启一个协程都会 new 新的迭代器对象,产生 GC Alloc。

UniTask 是基于 async/await 来实现的,可以替代协程。对于 async method,在 Release 模式下,编译器生成的 state machine 是值类型的。在此基础上,UniTask 又做了很多优化,使得它的 GC Alloc 很低,可以认为是 0 GC。