+
Update 8/10 2016 : Unity 5.3.5p8 導入了 Mono Compiler 的初步更新解決了這個問題,如果你的專案可以升級到 5.3.6 或是 5.4 之後的版本的話以下描述的問題就不會再發生了。
+
官方公告:
+
https://forum.unity.com/threads/upgraded-c-compiler-on-5-3-5p8.417363/
+
因為最近有討論區的朋友提到 foreach 的 Garbage Collection 問題,所以想寫一篇為什麼 foreach 會有 Garbage Collection 的文章。這篇文章比較無趣一些,TD;LR 的話就是問題是 IDisposable
不是 IEnumerator<T>
。
+
Garbage Collection
+
Unity 在使用 foreach 的時候會產生 24 bytes 的 GC 這個問題已經傳很久了。可以用個簡單的小程式去測試:
+
+
+1
+2
+3
+4
+5
+6
+7
+8
+
|
+
+class ForEachTest : MonoBehaviour
+{
+ private readonly List<int> list = new List<int> { 1, 2, 3 };
+ void Update()
+ {
+ foreach (var element in list) { }
+ }
+}
+
|
+
+
隨便掛在一個 GameObject 下面的執行結果,在 Unity 4.7.0f1
+
+
現在最新的 Unity 5.3.1f1 上面的結果好像更糟了:
+
+
兇手是誰?
+
我一直以為是因為 System.Collections.Generic
底下所有的容器的 Enumerator
都被宣告成 struct ,然後 foreach 在操作的時候卻是對 IEnumerator<T>
操作 IEnumerator<T>.Current
跟 IEnumerator<T>.MoveNext()
造成了 boxing 。這周末心血來潮把 Unity 建置出來的 dll 放進 ILSpy 裡面看看,才發現以往的認知是錯的。
+
+System.Collections.Generic 底下所有的容器的 Enumerator 都被宣告成 struct 的原因可以看 Eric Lippert(C# Compiler Team 的成員的解釋) :
+http://stackoverflow.com/questions/3168311/why-do-bcl-collections-use-struct-enumerators-not-classes/3168435#3168435
+基本上是效能考量
+
+
以下是範例程式
+
+
+1
+2
+3
+4
+5
+
|
+
+List<int> list = new List<int> { 1, 2, 3 };
+foreach(var item in list)
+{
+ Debug.Log(item);
+}
+
|
+
+
ILSpy 反組譯的結果
+
+
+ 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+
|
+
+List<int> list = new List<int>();
+list.Add(1);
+list.Add(2);
+list.Add(3);
+List<int> list2 = list;
+using (List<int>.Enumerator enumerator = list2.GetEnumerator())
+{
+ while (enumerator.MoveNext())
+ {
+ int current = enumerator.get_Current();
+ Debug.Log(current);
+ }
+}
+
|
+
+
可以看到實際上 Unity 其實正確地使用 List<int>.Enumerator
來承接 list.GetEnumerator()
的回傳值。所以那個 boxing 到底在哪裡呢?
+
有了這條線索後,Google 了一下發現已經有人找到了真正的問題。
+
https://www.reddit.com/r/Unity3D/comments/34s0je/c_memory_and_performance_tips_for_unity/cqyf5yk/
+
要看到問題要把 ILSpy 的展示模式從 C# 換成 IL 模式。
+
+
+ 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
+
|
+
+IL_001e: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> class [mscorlib]System.Collections.Generic.List`1<int32>::GetEnumerator()
+IL_0023: stloc.2
+.try
+{
+ IL_0024: br IL_003c
+ // loop start (head: IL_003c)
+ IL_0029: ldloca.s 2
+ IL_002b: call instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::get_Current()
+ IL_0030: stloc.1
+ IL_0031: ldloc.1
+ IL_0032: box [mscorlib]System.Int32
+ IL_0037: call void [UnityEngine]UnityEngine.Debug::Log(object)
+ IL_003c: ldloca.s 2
+ IL_003e: call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::MoveNext()
+ IL_0043: brtrue IL_0029
+ // end loop
+ IL_0048: leave IL_0059
+} // end .try
+finally
+{
+ IL_004d: ldloc.2
+ IL_004e: box valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>
+ IL_0053: callvirt instance void [mscorlib]System.IDisposable::Dispose()
+ IL_0058: endfinally
+} // end handler
+
|
+
+
可以看到 box 出現在 IL_004e 行 finally 區塊裡,結果是舊版的 Mono 對有實作 IDisposable
的 struct 呼叫 Dispose 的時候(using 關鍵字觸發的)用了 IDisposable
去 box ,這跟我之前以為的不一樣。
+
然後更冤的可以看一下 List<T>.Enumerator
的 Dispose 實作:
+
http://referencesource.microsoft.com/#mscorlib/system/collections/generic/list.cs,d3661cf752ff3f44
+
因為 List<T>.Enumerator
是 value type ,所以根本就不需要特別處理。這個 Dispose 是空函式,整個 boxing 是 100% 的浪費。
+
Mono 對於這個 bug 的 issue 在這裡:
+
https://bugzilla.novell.com/show_bug.cgi?id=571010
+
可以看到 Mono 本家已經在 2010 6/1 修正了了這個問題,但是 Unity 還是沒有 merge 這個修正。考慮到 Unity 自己有 Mono 的 fork (https://github.com/Unity-Technologies/mono),很有可能 Unity 有對 Mono 做修改,改動到現在合併有困難。否則大家喊很久的 Mono 升級或是改用 Roslyn ,為什麼 Unity 一直無法從善如流。
+
我自己對於 foreach 的態度就是雖然效能較差還有少量 GC 問題,但是做取捨我還是會選 foreach 取其可讀性。要小心的是如果 foreach 放在其他的 loop 裡面的情況,累積起來還是有可能會造成問題。
+
參考資料:
+
C# memory and performance tips for Unity
+
原文:
+
http://www.somasim.com/blog/2015/04/csharp-memory-and-performance-tips-for-unity/
+
Reddit 討論:
+
https://www.reddit.com/r/Unity3D/comments/34s0je/c_memory_and_performance_tips_for_unity/
+
C# Memory Management for Unity Developers (part 1 of 3)
+
https://www.gamasutra.com/blogs/WendelinReich/20131109/203841/C_Memory_Management_for_Unity_Developers_part_1_of_3.php
+
Why do BCL Collections use struct enumerators, not classes?
+
http://stackoverflow.com/questions/3168311/why-do-bcl-collections-use-struct-enumerators-not-classes/3168435#3168435
+
ILSpy
+
http://ilspy.net/
+
+