分享

Unity3D里foreach,using和Coroutine的GC问题探究及解决方案 | Atlantis技术博客

 勤奋不止 2017-05-22

Foreach:

很多Unity3D的优化技巧甚至一些公司的笔试题中都会涉及foreach会产生GC Alloc因此游戏运行时中尤其是在Update里应尽量避免使用foreach的这个注意事项。

foreach真的会产生GC Alloc吗?我们作如下测试:(Unity3D 5.4.0)

创建脚本TestForeach.cs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
public class TestForeach : MonoBehaviour
{
private int[] _arr = new int[] { 1, 2, 3 };
private List<int> _list = new List<int>();
private Dictionary<int, int> _dic = new Dictionary<int, int>();
//private IEnumerator _i;
private Dictionary<int, int>.Enumerator _i;
void Start()
{
_list.Add(1);
_list.Add(2);
_list.Add(3);
_dic.Add(1, 1);
_dic.Add(2, 2);
_dic.Add(3, 3);
}
void Update()
{
#region Array
// code Array for
//for (int ii = 0; ii < _list.Count; ++ii)
//{}
// code Array foreach
//foreach (int i in _arr)
//{}
#endregion
#region List
// code List for
//for (int ii = 0; ii < _list.Count; ++ii)
//{}
// code List foreach
//foreach (int i in _list)
//{}
        // 上面foreach代码被unity C#编译器编译后等价于:
        //IEnumerator iter = _list.GetEnumerator();
        //while (iter.MoveNext())
        //{ }
        // 上面foreach代码被unity5.3.5p8 C#编译器编译后等价于:
        //List<int>.Enumerator iter = _list.GetEnumerator();
        //while (iter.MoveNext())
        //{ }
//_i = _list.GetEnumerator();
//while (_i.MoveNext())
//{}
#endregion
#region Dictionary
//foreach (var m in _dic)
//{}
//_i = _dic.GetEnumerator();
//while (_i.MoveNext())
//{}
        //using(Dictionary<int, int>.Enumerator iter = _dic.GetEnumerator())
        //{
        //    while (iter.MoveNext())
        //    { }
        //}
        // 上面using代码被unity C#编译器编译后等价于:
        //Dictionary<int, int>.Enumerator iter = _dic.GetEnumerator();
        //try
        //{ }
        //finally
        //{
        //    ((IDisposable)iter).Dispose();
        //}
        // 上面代码被unity5.3.5p8 编译器编译后等价于:
        //Dictionary<int, int>.Enumerator iter = _dic.GetEnumerator();
        //try
        //{ }
        //finally
        //{
        //    iter.Dispose();
        //}
#endregion
}
}

将脚本挂在新建空场景的摄像机上,依次将每个代码块的注释去掉,打开Profiler,查看CPU模块,并按GC Alloc项排序,得到如下结果:

 

Unity3D里foreach,using和Coroutine的GC问题探究及解决方案 - 第1张  | Atlantis技术博客

Array for

 

Unity3D里foreach,using和Coroutine的GC问题探究及解决方案 - 第2张  | Atlantis技术博客

Array foreach

 

Unity3D里foreach,using和Coroutine的GC问题探究及解决方案 - 第3张  | Atlantis技术博客

List for

 

Unity3D里foreach,using和Coroutine的GC问题探究及解决方案 - 第4张  | Atlantis技术博客

List foreach

 

Unity3D里foreach,using和Coroutine的GC问题探究及解决方案 - 第5张  | Atlantis技术博客

List GetEnumerator

 

Unity3D里foreach,using和Coroutine的GC问题探究及解决方案 - 第6张  | Atlantis技术博客

Dictionary foreach

 

Unity3D里foreach,using和Coroutine的GC问题探究及解决方案 - 第7张  | Atlantis技术博客

Dictionay GetEnumerator

从以上可以看出foreach对数组是没有GC Alloc的,但是对List和Dictionary容器是有GC Alloc的,而且foreach和迭代产生的GC Alloc都是40B。

在C#中,foreach语句其实是微软提供的语法糖,使用它可以简化C#内置迭代器的使用复杂性。编译器在编译foreach语句时会生成调用GetEnumerator和MoveNext方法以及Current属性的代码,这些代码和属性恰是C#内置迭代器所提供的,所以上面的foreach语句和相应迭代器语句是等价的,所以GC Alloc也是相同的。

那么迭代器语句为什么会产生GC Alloc呢?以Dictionay为例(同样适用于List),_dic.GetEnumerator()返回的是Dictionary<int, int>.Enumerator,这是一个Struct,而_i是一个IEnumerator类型的引用,这里就需要将一个值类型变量转为一个引用类型变量,就需要执行一次装箱操作,而装箱操作是需要额外耗费CPU和内存资源的(参考:http://www.cnblogs.com/yukaizhao/archive/2011/10/18/csharp_box_unbox_1.html),所以就会产生GC Alloc。

那么我们把_i声明称Dictionary<int, int>.Enumerator类型,就不会有装箱操作了,那么还会有GCAlloc吗?测试结果如下:

Unity3D里foreach,using和Coroutine的GC问题探究及解决方案 - 第8张  | Atlantis技术博客

private Dictionary<int, int>.Enumerator _i;

真的就没有GC Alloc了!

那么foreach为什么不生成这种不需要装箱操作的代码呢?这其实是早期Mono C#编译器的一个bug,后来的版本修复了,但是目前Unity3D用的还是未修复的版本。。。

官方针对Unity5.3.5f1提供了一个升级包,可以升级到5.3.5p8,里面包含对Mono C#编译器的升级,可以升级到Mono 4.4来修复这个问题,链接地址:

http://forum./threads/upgraded-c-compiler-on-5-3-5p8.417363/

将这个升级包安装到Unity3D 5.3.5f1后会发现foreach确实不会再有GC Alloc了~,但是经测试5.3.6及目前最新的5.4.0中foreach的GC Alloc问题还存在,猜测这个升级包应该只是单独针对5.3.5测试用,还未正式对后续Unity3D版本的Mono C#编译器升级。那我们是否可以自己升级编译器版本呢?当然可以,可以仿照https:///jbruening/unity-c-5.0-and-6.0-integration提供的方法来升级到比较新的编译器。

还有一种折中的解决办法,一般来说我们的C#逻辑代码会编译进Assembly-CSharp.dll里,这个编译过程是Unity3D用指定的C#编译器自动进行的,那我们把逻辑代码单独用VS编译成dll然后让Assembly-CSharp.dll引用是不是就不会产生GC Alloc了呢?经测试这方法可行:)~

另外经测试,上述using()用法也会产生GC Alloc,也是拆箱装箱导致的(具体看上面代码),同样在5.3.5p8编译器升级后就不会有GC Alloc了。

所以就foreach会产生GC Alloc的问题建议如下:

1.对List和Dictionary等容器的遍历不使用foreach,可以用for和迭代器(上面优化过的),对于数组foreach可以放心使用:)

2.对于Unity3D 5.3.5f1版本可以尝试安装官方提供的5.3.5p8升级包,就可以放心使用foreach和using了:)

3.升级C#编译器版本(只是针对当前项目,并不是升级Unity自己的C#编译器)

4.逻辑代码单独用VS编译成dll,就可以放心使用foreach和using了

5.如果你不是5.3.5版本也不想吧代码单独编译成dll,但还是想用foreach和using(以为比较方便嘛),那么建议一定不要在Update里使用!!!

 

Coroutine:

Coroutine也会产生GC Alloc吗?我们做如下测试:(Unity3D 5.3.5f1,至于为什么选择这个版本,看完下面你就知道了:))

创建代码TestCoroutine.cs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using UnityEngine;
using System.Collections;
public class TestCoroutine : MonoBehaviour
{
void Start()
{
StartCoroutine(_UnityCoroutine());
}
IEnumerator _UnityCoroutine()
{
while (true)
{
yield return null;
}
}
}

将脚本挂在新建空场景的摄像机上,打开Profiler,查看CPU模块,并按GC Alloc项排序,得到如下结果:

Unity3D里foreach,using和Coroutine的GC问题探究及解决方案 - 第9张  | Atlantis技术博客

看来Coroutine确实会产生GC Alloc,不过这个问题在5.3.6版本中修复了,查看5.3.6版本的发行说明(https:///cn/unity/whats-new/unity-5.3.6)会发现:

Unity3D里foreach,using和Coroutine的GC问题探究及解决方案 - 第10张  | Atlantis技术博客

经测试发现5.3.6及最新的5.4.0的Coroutine确实不会再产生GC Alloc了,另外经测试发现安装上面提到的5.3.5f1的升级包升级至5.3.5p8后这个GC问题也没有了。

那么使用5.3.5之前版本的开发者该怎么办呢?答案是不再使用Unity3D的Coroutine,自己实现一套。。。不过为了避免重复造轮子在AssetStore上找到一个替代方案More Effective Coroutines(简称MEC):https://www.assetstore./en/#!/content/54975,正如它的名字所说,它避免了GC的问题所以更高效,插件里有详细教程,这里不再赘述,直接做测试:

将TestCoroutine.cs修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using UnityEngine;
using System.Collections.Generic;
using MovementEffects;
public class TestCoroutine : MonoBehaviour
{
void Start()
{
Timing.RunCoroutine(_UnityCoroutine());
}
IEnumerator<float> _UnityCoroutine()
{
while (true)
{
yield return 0;
}
}
}

测试结果如下:

Unity3D里foreach,using和Coroutine的GC问题探究及解决方案 - 第11张  | Atlantis技术博客

确实不会产生GC Alloc了,所以对于5.3.5之前的版本可以使用此插件来替代Unity3D的Coroutine,另外有几点需要注意的地方:

1.由于此插件中用到的部分API只有Unity5.x才有所以如果使用Unity4.x需要将用到这些API的代码用#if UNITY_5 #endif包一下禁用掉。

2.如果直接使用Timing.RunCoroutine的话,需要先保存下他的返回,在当前脚本要销毁时在OnDestroy()里加下Timing.KillCoroutines(前面的返回),如果是直接在当前脚本的gameObject上挂一个Timing组件并且gameObject.GetComponent<Timing>.RunCoroutine的话,在销毁时就不用再KillCoroutines了,因为它会自动销毁~

 

 

最后编辑:
作者:maosongliang
这个作者貌似有点懒,什么都没有留下。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多