Unity优化记录(3)——C#(如何解决使用enum和struct作为Dictionary的TKey带来的GC)

1、在Dictionary中使用Enum作为TKey会带来GC

这个问题是在阅读《2018腾讯移动游戏技术评审标准与实践案例》中发现的,在原书第21页有如下测试代码和测试结果:
Unity优化记录(3)——C#(如何解决使用enum和struct作为Dictionary的TKey带来的GC)_第1张图片
Unity优化记录(3)——C#(如何解决使用enum和struct作为Dictionary的TKey带来的GC)_第2张图片
在unity中得到的详细信息:
Unity优化记录(3)——C#(如何解决使用enum和struct作为Dictionary的TKey带来的GC)_第3张图片
可以看到,GC是来自DefaultComparer.GetHashCode()的,在一个Dictionary中,如果没有指定Comparer时,会通过EqualityComparer.Default来创建一个默认的Comparer,对于enum类型的数据,最后产生的是下图中红框所标注出来的Comparer:
Unity优化记录(3)——C#(如何解决使用enum和struct作为Dictionary的TKey带来的GC)_第4张图片
而对于这个类的Equals方法和GetHashCode方法,在比较时会将目标转为object类型
Unity优化记录(3)——C#(如何解决使用enum和struct作为Dictionary的TKey带来的GC)_第5张图片
Unity优化记录(3)——C#(如何解决使用enum和struct作为Dictionary的TKey带来的GC)_第6张图片
而enum实际上在IL中是以值类型的方式存在的:
Unity优化记录(3)——C#(如何解决使用enum和struct作为Dictionary的TKey带来的GC)_第7张图片
所以这里就时由于调用了一次GetHashCode发生了一次装箱,一个int为4字节,一个object本身占16字节,所以就产生了上图所示的20B的GC,同理调用一次Equals会产生40B的GC。
而如果ContainsKey中查找的key是存在的,就会同时调用GetHashCode和Equals,产生60B的GC:Unity优化记录(3)——C#(如何解决使用enum和struct作为Dictionary的TKey带来的GC)_第8张图片
所以这里就可以得到解决方案了,那就是自己实现一个comparer在构造时传入,让两个enum直接以int的方式来进行比较:
Unity优化记录(3)——C#(如何解决使用enum和struct作为Dictionary的TKey带来的GC)_第9张图片
这样就可以让两个enum依旧以值类型的方式进行比较和GetHashCode,就不会产生GC了:
在这里插入图片描述
根据以上分析,我们可以发现,产生GC的实际原因是自定义的值类型没有Equals和GetHashCode方法,导致Dictionary在作比较时只能将值类型装箱(box)为引用类型,从而导致了GC,那么为什么直接使用int时不会产生GC呢?继续做一个测试,当使用int作为TKey时,得到如下结果:
在这里插入图片描述
可以看到,生成的是一个GenericEqualityComparer,翻阅默认Comparer源码,要生成这个Comparer的条件是:
在这里插入图片描述
也就是说,如果TKey实现了IEquatable接口,就会生成一个GenericEqualityComparer作为comparer,翻阅int源码,我们可以很清晰得找到这个接口:
在这里插入图片描述
所以,接下来,我们自行创建一个struct,分别在不实现IEquatable和实现IEquatable的情况下进行测试:
不实现IEquatable:
Unity优化记录(3)——C#(如何解决使用enum和struct作为Dictionary的TKey带来的GC)_第10张图片
在这里插入图片描述

实现IEquatable:
Unity优化记录(3)——C#(如何解决使用enum和struct作为Dictionary的TKey带来的GC)_第11张图片
在这里插入图片描述
由如上测试可以得到结论,当某个自定义的值类型作为Dictionary的TKey时,如果不实现IEquatable接口,生成的DefaultComparer时就会将其装箱(box)来做比较,而如果实现了IEquatable接口,生成就是GenericEqualityComparer,由于这个Comparer会使用TKey的那个值类型的GetHashCode和Equals方法,所以只要你实现的这两个方法不会产生GC,最终就不会产生GC。

更新:

在翻阅复习C#中其他几种数据结构时发现在Array中也是用过EqualityComparer来实现IndexOf方法的:
Unity优化记录(3)——C#(如何解决使用enum和struct作为Dictionary的TKey带来的GC)_第12张图片
Unity优化记录(3)——C#(如何解决使用enum和struct作为Dictionary的TKey带来的GC)_第13张图片
可以看到,如果是值类型的数据同样会发生装箱,因此包括Stack、Queue、LinkedList、List等在内的数据结构由于在构造器阶段无法传入EqualityComparer,所以如果将未实现IEqualityComparer的struct作为T的话就必定会在查找索引时产生GC。
而如果使用未实现IEqualityComparer的struct创建一个Array时,一旦调用IndexOf一类的方法时也会产生GC。

解决方案

  1. 对于enum类型,由于无法使其实现IEquatable接口,所以只能自定义一个class或者struct(建议使用struct,不会产生GC)实现IEqualityComparer,然后创建一个实例在Dictionary构造时传入。
  2. 对于struct类型,如果是第三方的无法修改,只能同样以第一种创建IEqualityComparer的方式来优化,如果是自定义的可以修改的struct,则可以通过直接实现IEquatable接口来避免GC。

注:
在更高等级的C#版本中创建DefaultComparer时针对enum类型做了新的处理,但是Unity内的C#版本常常比较落后,因此还是需要注意。Unity优化记录(3)——C#(如何解决使用enum和struct作为Dictionary的TKey带来的GC)_第14张图片

你可能感兴趣的:(Unity,深入了解C#)