Unity Foreach 内存分配调查

发表于2016-05-03
评论0 1.11w浏览
  Unity开发移动游戏,使用基于Mono的脚本代码,GC对于游戏帧率平滑是很重要的一环。官方文档说明集合类的遍历如List,若使用foreach会分配内存,建议使用for来代替。开始我认为是会分配迭代器(C#里应该叫枚举器吧)对象,也有说法是不存在这种情况。下面是一系列测试来验证foreach是否会分配内存以及为什么。
  能被foreach遍历的集合类,需要继承于IEnumerable或IEnumerable,并实现其接口IEnumerator GetEnumerator()。查看List的代码文件,实现了publicList.Enumerator GetEnumerator();并且实现了枚举器struct Enumerator结构体。对于值类型,使用new来分配空间,也是在栈上分配,不会在堆上分配,所以获取枚举器时不会在堆上分配内存。
  在Unity添加测试脚本,在Update里使用foreach遍历,Profiler查看每帧foreach的GC Alloc


  结果是在Update有GC,而GetEnumerator,MoveNext,Dispose,get_Current并没有GC,验证了获取枚举器不会分配内存。使用ildasm查看foreach的IL代码:


但是没有Newobj,Newarr或Box等分配对象的指令。
for循环方式遍历集合


没有分配对象的指令。


使用枚举器遍历集合类
            List.Enumerator enumerator = myList.GetEnumerator();
            while (enumerator.MoveNext())
            {
               int k = enumerator.Current;
            }


没有GC。IL代码如下:


没有分配对象的指令。对比foreach遍历与枚举器遍历,前者有GC,而后者没有,foreach遍历:


枚举器遍历:


不同之处在foreach遍历时调用了 Dispose,比较IL代码,foreach遍历多了try finally


问题就应该在这里,查看msdn文档。
http://msdn.microsoft.com/zh-cn/library/system.reflection.emit.opcodes.constrained.aspx


List的枚举器定义


是值类型。
  constrained 前缀设计用来使 callvirt 指令以统一方式出现,无论 thisType 是值类型还是引用类型。这么做的原因是值类型thisType ,如果要调用基类的虚方法,thisType若实现了此方法,可以执行call,注意不是callvirt;若thisType没有实现此方法,是不能成功执行callvirt指令的。constrained指令会根据类型确保后面的callvirt会调用成功,有了constrained指令,CLR会自动在执行callvirt时对值类型变量进行装箱,此时便在堆上分配对象了!!
  constrained 最主要的目的是为了泛型类的统一。
  为什么foreach遍历会有try finally这些额外的代码?
  List的枚举器Enumerator继承于IDisposable,再查看IEnumerator


  也继承于IDisposable。能被foreach遍历的集合类,必须实现IEnumerator或IEnumerator。尝试集合类里的Enumerator不继承于IDisposable,如此当然也不能继承于IEnumerator了,因为IEnumerator继承于IDisposable。
  自定义一个集合类,不能是泛型,由于不能继承于IEnumerator。自定义集合类的枚举器public struct Enumerator : IEnumerator


在Update方法里使用foreach


没有GC了。查看IL代码:


  没有try finally代码。所以结论是枚举器继承了IDisposable,编译器就会生成try finally代码,也就会在堆上分配对象。
  最后再做一次无聊的测试,把自定义的集合类改成支持泛型,同时将库文件上的IEnumerable,IEnumerable,IEnumerator的定义拷贝到自定义集合类代码文件中。稍作修改,IEnumerable不继承于IDisposable。根据命名空间作用域就近原则,这些接口定义将屏蔽系统的接口,则自定义的集合类就继承于当前命名空间中的接口。


红线框内IEnumerable不继承于IDisposable。Profiler查看:


GC没了。。。
  查看了IL代码,自然没有了try finally这些代码,不贴出来了。所以可以写一个自定义集合泛型类,使用foreach也不会有GC。除非有使用foreach的习惯,否则使用for或枚举器遍历就ok了,意义不大。若项目已经使用了foreach,写一个自定义类来替换之前的集合类,倒也是一个选择。
  HashTable,ArryList都是以object对象存储的,装箱拆箱代价太大。Dictionary<>类型也不能使用foreach,但同样可以使用枚举器遍历。
        Dictionary.Enumerator enumerator = dicTest.GetEnumerator();
        while (enumerator.MoveNext())
        {
            int key = enumerator.Current.Key;
            TestObj obj = enumerator.Current.Value;
        }
  请注意不要用任何基类类型来定义enumerator变量,如IEnumerator> enumerator = dicTest.GetEnumerator();这会造成装箱操作。


  原因是Interface的实现最终是由抽象类来实现的,值类型的值赋值给基类类型,便会装箱。
  文中若有错误,请指正!


如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引