2017.02 Unity GC Cheatsheet

Views

关于 Unity 的垃圾回收 (GC) 你可能已经看到不少的文章讨论了。

下面是一个极简形式的 Cheatsheet,希望能在最小的篇幅内尽可能全面地列出关于 GC 你需要注意的事项。


Unity GC Cheatsheet



  • b01. 避免频繁调用分配内存的 accessors (如 .vertices/.normals/.uvs/.bones)
  • b02. 避免频繁调用 Int.ToString() 及其它类型的衍生
  • b03. 避免在 Update() 内使用 GameObject.TagGameObject.Name
  • b04. 避免在 Update() 内 GetComponent()
  • b05. 避免在 Update() 内 GetComponentInChildren(),可自己实现无 GC 版本
  • b06. 避免在 Update() 内访问 animation 组件
  • b07. 避免在 Update() 内 FindObjectsOfType()
  • b08. 避免在 Update() 里赋值给栈上的数组,会触发堆内的反复分配
  • b09. 避免频繁使用 Mathf.Max 等函数的数组版(多于两个参数都会调到数组版)
  • b10. (b09 推广):所有具有 params 修饰的函数都应避免频繁使用(以避免临时数组的分配)

  • c01. 在不需要时避免使用 GUILayout - OnGUI 时把 useGUILayout 关掉
  • c02. 避免使用 foreach (除非遍历数组,或直接用 VS 预编译好的 dll)(Unity 5.5 已修复此问题)
  • c03. 避免使用枚举或 struct 做 Key 进行字典查找 (除非使用定制的 comparer)
  • c04. 避免使用字符串版本的 Invoke 和 StartCoroutine
  • c05. 避免在产品中调用 Debug.Log (生成堆栈字符串)
  • c06. 避免在产品中使用 Linq
  • c07. 在可能的情况下复用成员变量而不是不断分配新对象
  • c08. 初始化 List<> 时指定合理的 Capacity
  • c09. 使用 StringBuilder 而不是使用 “+” 或 String.Concat() 拼接字符串
  • c10. 在使用协程 yield 时尽量复用 WaitXXX 对象 (使用 Yielders.cs) 而不是每次分配
  • c11. 确保 struct 实现了 IEquatable
  • c12. 确保 struct 实现了 Equals() 和 GetHashCode()

Details & Explanations

a05. 在针对 GC Alloc 做优化时,对象数量 > 引用关系复杂度 > 对象尺寸

Boehm garbage collector 而言,对象数量直接影响单次 GC 的时间开销 每个对象 90 个时钟周期左右 (大量时间是 cache-missing 所致) 算下来每秒 15M 数目的对象,也就是每毫秒标记 15000 个左右

a07. 优化内存布局:利用“数组对 GC 而言是单一对象”这一特性

如我们有 List,内含 100 个对象。其中,Foo 如下定义

1
2
3
4
5
6
7
class Foo
{
    int a;
    float b;
    bool c;
    string str;
}

此时内存中共有 101 个 GC 对象 (100 个 Foo + 1 个 List 内部数组) ,且为 2 级的引用关系 假如我们把数据打散成为单独的数组,如下所示:

1
2
3
4
int[] aArray;
float[] bArray;
bool[] cArray;
string[] strArray;

此时所有数据不变的情况下,对象数量 (对 GC 而言) 降低到了 4 个 更进一步,我们把所有的 ValueType 聚合起来

1
2
3
4
5
6
struct Foo_S
{
    int a;
    float b;
    bool c;
}

数据结构就成了

1
2
Foo_S[] fooArray;
string[] strArray;

此时所有数据不变的情况下,对象数量 (对 GC 而言) 降低到了 2 个

c03. 避免使用枚举或 struct 做 Key 进行字典查找 (除非使用定制的 comparer)

当 Key 为用户定义的 struct 而非内建的值类型时,Dictionary 的主要接口会产生 GC Alloc Add / ContainsKey / TryGetValue / “[ ]” 等接口都需要对传进来的 TKey 调用默认的 EqualityComparer 来判断是否相等 见 .net 代码文件 dictionary.cs 的第 94 行:

1
this.comparer = comparer ?? EqualityComparer<TKey>.Default;

而 EqualityComparer 的内部调了私有的 CreateComparer() 来创建真实的 Comparer,见 EqualityComparer.cs

1
private static EqualityComparer<T> CreateComparer() {}

内建类型(int/float 等等)已经实现了良好的 Equality 判断,而用户定义的 struct 则没有。很可惜上面的代码是 .Net 4.6 的最新代码,有理由推断老版本 mono 在对用户定义的 struct 调用上面的 Add / ContainsKey / TryGetValue / “[ ]” 等接口时产生了内存分配。

方案:只需要手动定义一下 Comparer 并实现 Equals() 和 GetHashCode() 即可

References

官方及第三方参考 (En)

侑虎 (UWA)

腾讯质量开放平台 (WeTest)

第三方内存工具

知乎问答

调查和诊断过程

其他

(全文完)


comments powered by Disqus
Built with Hugo