Foreach:
很多Unity3D的優(yōu)化技巧甚至一些公司的筆試題中都會(huì)涉及foreach會(huì)產(chǎn)生GC Alloc因此游戲運(yùn)行時(shí)中尤其是在Update里應(yīng)盡量避免使用foreach的這個(gè)注意事項(xiàng)。
foreach真的會(huì)產(chǎn)生GC Alloc嗎?我們作如下測試:(Unity3D 5.4.0)
創(chuàng)建腳本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#編譯器編譯后等價(jià)于:
//IEnumerator iter = _list.GetEnumerator();
//while (iter.MoveNext())
//{ }
// 上面foreach代碼被unity5.3.5p8 C#編譯器編譯后等價(jià)于:
//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#編譯器編譯后等價(jià)于:
//Dictionary<int, int>.Enumerator iter = _dic.GetEnumerator();
//try
//{ }
//finally
//{
// ((IDisposable)iter).Dispose();
//}
// 上面代碼被unity5.3.5p8 編譯器編譯后等價(jià)于:
//Dictionary<int, int>.Enumerator iter = _dic.GetEnumerator();
//try
//{ }
//finally
//{
// iter.Dispose();
//}
#endregion
}
}
將腳本掛在新建空場景的攝像機(jī)上,依次將每個(gè)代碼塊的注釋去掉,打開Profiler,查看CPU模塊,并按GC Alloc項(xiàng)排序,得到如下結(jié)果:
Array for
Array foreach
List for
List foreach
List GetEnumerator
Dictionary foreach
Dictionay GetEnumerator
從以上可以看出foreach對數(shù)組是沒有GC Alloc的,但是對List和Dictionary容器是有GC Alloc的,而且foreach和迭代產(chǎn)生的GC Alloc都是40B。
在C#中,foreach語句其實(shí)是微軟提供的語法糖,使用它可以簡化C#內(nèi)置迭代器的使用復(fù)雜性。編譯器在編譯foreach語句時(shí)會(huì)生成調(diào)用GetEnumerator和MoveNext方法以及Current屬性的代碼,這些代碼和屬性恰是C#內(nèi)置迭代器所提供的,所以上面的foreach語句和相應(yīng)迭代器語句是等價(jià)的,所以GC Alloc也是相同的。
那么迭代器語句為什么會(huì)產(chǎn)生GC Alloc呢?以Dictionay為例(同樣適用于List),_dic.GetEnumerator()返回的是Dictionary<int, int>.Enumerator,這是一個(gè)Struct,而_i是一個(gè)IEnumerator類型的引用,這里就需要將一個(gè)值類型變量轉(zhuǎn)為一個(gè)引用類型變量,就需要執(zhí)行一次裝箱操作,而裝箱操作是需要額外耗費(fèi)CPU和內(nèi)存資源的(參考:
http://www.cnblogs.com/yukaizhao/archive/2011/10/18/csharp_box_unbox_1.html),所以就會(huì)產(chǎn)生GC Alloc。
那么我們把_i聲明稱Dictionary<int, int>.Enumerator類型,就不會(huì)有裝箱操作了,那么還會(huì)有GCAlloc嗎?測試結(jié)果如下:
private Dictionary<int, int>.Enumerator _i;
真的就沒有GC Alloc了!
那么foreach為什么不生成這種不需要裝箱操作的代碼呢?這其實(shí)是早期Mono C#編譯器的一個(gè)bug,后來的版本修復(fù)了,但是目前Unity3D用的還是未修復(fù)的版本。。。
官方針對Unity5.3.5f1提供了一個(gè)升級(jí)包,可以升級(jí)到5.3.5p8,里面包含對Mono C#編譯器的升級(jí),可以升級(jí)到Mono 4.4來修復(fù)這個(gè)問題,鏈接地址:
http://forum.unity3d.com/threads/upgraded-c-compiler-on-5-3-5p8.417363/將這個(gè)升級(jí)包安裝到Unity3D 5.3.5f1后會(huì)發(fā)現(xiàn)foreach確實(shí)不會(huì)再有GC Alloc了~,但是經(jīng)測試5.3.6及目前最新的5.4.0中foreach的GC Alloc問題還存在,猜測這個(gè)升級(jí)包應(yīng)該只是單獨(dú)針對5.3.5測試用,還未正式對后續(xù)Unity3D版本的Mono C#編譯器升級(jí)。那我們是否可以自己升級(jí)編譯器版本呢?當(dāng)然可以,可以仿照
https://bitbucket.org/jbruening/unity-c-5.0-and-6.0-integration提供的方法來升級(jí)到比較新的編譯器。
還有一種折中的解決辦法,一般來說我們的C#邏輯代碼會(huì)編譯進(jìn)Assembly-CSharp.dll里,這個(gè)編譯過程是Unity3D用指定的C#編譯器自動(dòng)進(jìn)行的,那我們把邏輯代碼單獨(dú)用VS編譯成dll然后讓Assembly-CSharp.dll引用是不是就不會(huì)產(chǎn)生GC Alloc了呢?經(jīng)測試這方法可行:)~
另外經(jīng)測試,上述using()用法也會(huì)產(chǎn)生GC Alloc,也是拆箱裝箱導(dǎo)致的(具體看上面代碼),同樣在5.3.5p8編譯器升級(jí)后就不會(huì)有GC Alloc了。
所以就foreach會(huì)產(chǎn)生GC Alloc的問題建議如下:
1.對List和Dictionary等容器的遍歷不使用foreach,可以用for和迭代器(上面優(yōu)化過的),對于數(shù)組foreach可以放心使用:)
2.對于Unity3D 5.3.5f1版本可以嘗試安裝官方提供的5.3.5p8升級(jí)包,就可以放心使用foreach和using了:)
3.升級(jí)C#編譯器版本(只是針對當(dāng)前項(xiàng)目,并不是升級(jí)Unity自己的C#編譯器)
4.邏輯代碼單獨(dú)用VS編譯成dll,就可以放心使用foreach和using了
5.如果你不是5.3.5版本也不想吧代碼單獨(dú)編譯成dll,但還是想用foreach和using(以為比較方便嘛),那么建議一定不要在Update里使用?。?!
Coroutine:
Coroutine也會(huì)產(chǎn)生GC Alloc嗎?我們做如下測試:(Unity3D 5.3.5f1,至于為什么選擇這個(gè)版本,看完下面你就知道了:))
創(chuàng)建代碼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;
}
}
}
將腳本掛在新建空場景的攝像機(jī)上,打開Profiler,查看CPU模塊,并按GC Alloc項(xiàng)排序,得到如下結(jié)果:
看來Coroutine確實(shí)會(huì)產(chǎn)生GC Alloc,不過這個(gè)問題在5.3.6版本中修復(fù)了,查看5.3.6版本的發(fā)行說明(https://unity3d.com/cn/unity/whats-new/unity-5.3.6)會(huì)發(fā)現(xiàn):
經(jīng)測試發(fā)現(xiàn)5.3.6及最新的5.4.0的Coroutine確實(shí)不會(huì)再產(chǎn)生GC Alloc了,另外經(jīng)測試發(fā)現(xiàn)安裝上面提到的5.3.5f1的升級(jí)包升級(jí)至5.3.5p8后這個(gè)GC問題也沒有了。
那么使用5.3.5之前版本的開發(fā)者該怎么辦呢?答案是不再使用Unity3D的Coroutine,自己實(shí)現(xiàn)一套。。。不過為了避免重復(fù)造輪子在AssetStore上找到一個(gè)替代方案More Effective Coroutines(簡稱MEC):
https://www.assetstore.unity3d.com/en/#!/content/54975,正如它的名字所說,它避免了GC的問題所以更高效,插件里有詳細(xì)教程,這里不再贅述,直接做測試:
將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;
}
}
}
測試結(jié)果如下:
確實(shí)不會(huì)產(chǎn)生GC Alloc了,所以對于5.3.5之前的版本可以使用此插件來替代Unity3D的Coroutine,另外有幾點(diǎn)需要注意的地方:
1.由于此插件中用到的部分API只有Unity5.x才有所以如果使用Unity4.x需要將用到這些API的代碼用#if UNITY_5 #endif包一下禁用掉。
2.如果直接使用Timing.RunCoroutine的話,需要先保存下他的返回,在當(dāng)前腳本要銷毀時(shí)在OnDestroy()里加下Timing.KillCoroutines(前面的返回),如果是直接在當(dāng)前腳本的gameObject上掛一個(gè)Timing組件并且gameObject.GetComponent<Timing>.RunCoroutine的話,在銷毀時(shí)就不用再KillCoroutines了,因?yàn)樗鼤?huì)自動(dòng)銷毀~
8
本文固定鏈接:
http://www.maosongliang.com/archives/285 轉(zhuǎn)載請注明:
maosongliang 2016年08月07日 于
Atlantis技術(shù)博客 發(fā)表
最后編輯:2016-09-25
作者:maosongliang
這個(gè)作者貌似有點(diǎn)懶,什么都沒有留下。
站內(nèi)專欄新浪微博