Northern Wind

Maybe you won't get some, maybe you will

Unity

Unity 的 foreach

Update 8/10 2016 : Unity 5.3.5p8 導入了 Mono Compiler 的初步更新解決了這個問題,如果你的專案可以升級到 5.3.6 或是 5.4 之後的版本的話以下描述的問題就不會再發生了。

官方公告:
http://forum.unity3d.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 這個問題已經傳很久了。可以用個簡單的小程式去測試:

隨便掛在一個 GameObject 下面的執行結果,在 Unity 4.7.0f1

Unity4.7Update

現在最新的 Unity 5.3.1f1 上面的結果好像更糟了:

unity5update

兇手是誰?

我一直以為是因為 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

基本上是效能考量

以下是範例程式

ILSpy 反組譯的結果

可以看到實際上 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 模式。

可以看到 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)
http://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/

 

2 Comments

  1. Jin

    > Update 8/10 2016 : Unity 5.3.5p8 導入了 Mono Compiler 的初步更新解決了這個問題,如果你的專案可以升級到 5.3.6 或是 5.4 之後的版本的話以下描述的問題就不會再發生了。

    這段資訊可能要更新一下,因為實測後發現5.4.0 / 5.4.1的版本都還有這個問題。
    對岸的朋友發現5.3.6的版本,這個問題依然存在,連結:
    http://www.maosongliang.com/archives/285

    所以看起來只有5.3.5p8修正了而已(已經不知道該說什麼才好)

    • franknine

      感謝告知,看來還是不能把官方的話當真 0rz

Leave a Reply

Theme by Anders Norén