作為Unity3D的腳本而言,c#中for是否真的比foreach效率更高?
網上會流傳一些說法是說在c#中for比foreach指令更加精簡,效率更高,而且foreach會在每次循環的時候產生幾kb的gc。所以在開發中(Unity)盡量使用for來替代foreach。
FOREACH Vs. FOR (C#)Unity裡面應盡量避免使用foreach : Shallway Studio請原諒我可恥的做個伸手黨,請問這個結論在現在依然有效嗎? 難道Mono或者微軟開發c#的工程師不會對其進行優化嗎?還是說這個是一個無法解決的問題?
## Update 2016.7.29
unity5.3.5p8出了個升級mono編譯器的版本,消除了這部分GC,具體可參考:C# Compiler - Upgraded C# Compiler on 5.3.5p8
感謝 @胡有成 同學評論中的信息。
就像下文中分析的,這就是Unity目前採用的C# Compiler的一個bug,所以解決方法完全可以通過只升級C# Compiler、而不用設計運行時更新來解決。
實際上社區內已經有用戶做了這部分嘗試並且很成功,甚至可以支持到C# 6的大部分特性(比如async!),比Unity釋出的當前這個版本還要激進。
alexzzzz / Unity C# 5.0 and 6.0 Integration/ source /
注意無論是Unity的這個版本,還是社區用戶自己擼的這套,都還是實驗性質。不過絕對是社區內期待已久的一大進步!
==================================
## Update 2016.6.21
關於這個問題,另一篇英文的blog講得更加深入,推薦給感興趣的同學:
Unity Mono Runtime
這篇blog里,對於產生額外gc的原因分析,和我原答案是基本一致的。有趣的點是:blog的作者進一步深挖,找到了Eric Lippert(曾經在微軟開發C#編譯器的大神)關於這個問題的一個回應:c# - When does a using-statement box its argument, when it"s a struct?,結論是:(消除額外gc的優化方式)實際上嚴格來說是違法C#語言規範的(actually strictly speaking a violation of the C# specification)
所以,其實mono也是被坑的 lol
==================================
「foreach會造成額外的gc開銷」這個坑,在Unity社區里已經是個常識。
避免這個坑的方式,是盡量改寫為 等價於foreach的for/while代碼 來避免額外的gc開銷。關於這個坑的深層原因,在網上很早就有詳盡的分析,比如:
Memory Management for Unity Developers 中提到的「Should you avoid foreach loops?」然而關於這個現象的解釋,還是有很多「玄學」在到處飛揚,包括題主提到的「for比foreach指令更加精簡,更加高效」等等,終於也在知乎看到了。。。
其實,真相當然只有一個:這就是個bug!
以下是我重複了上面那個引文中關於這個bug的分析步驟,同時也是為了驗證一下這一bug是否已經在Unity 5以及il2cpp中解決。
=====================
首先是Unity 5.0.2中寫的一段測試代碼:(_tl是一個List&
- foreach會導致額外的gc對象,每次40B
- 這個bug在Unity 5(使用mono的情況下)依然存在
=====================
那麼額外的gc對象是什麼呢?肯定是foreach被展開成了什麼。。。
那就把Unity編出來的Assembly-CSharp.dll(Unity工程目錄下LibraryScriptAssemblies中)拉到任意一個反編譯工具中看一下唄
恩,foreach這個語法糖被正確展開成while了。這都是符合.Net常識的。這裡再提一下 @權然 答案中提到的GetEnumerator的黑歷史。這部分歷史是真實存在的,GetEnumerator現在是返回struct了。然而問題並不出在這裡,否則沒法解釋我們僅僅通過將foreach改寫為while,就能消除多餘的gc對象。
那還有可能出問題的呢?當然只剩下那個using!
這裡需要再深入一層,於是我們來看一下生成的最終IL代碼:真相就已經出現了:在finally里,mono編譯出來的代碼中有一次將valuetype的Enumerator,boxing的過程!!"What a waste!!!"
這就是Unity中所帶的老版本mono編譯器的一個bug!!!
=====================
關於il2cpp的結果:先說結論,在Unity 4.x的語境下這個觀點是正確的,能用for就別用foreach。
背景:
foreach會在託管堆上分配內存的問題在早期的C#中也是存在的,原因是foreach會將迭代器轉換為IEnumerator。如果迭代器是引用類型,自然會分配在託管堆上;如果是值類型,值類型轉換到介面類型是要裝箱(boxing)的,需要在託管堆上分配內存並將數據拷貝過去。橫豎都躲不過。後來微軟在編譯器中把這個問題優化掉了,辦法是編譯時查找名字叫做GetEnumerator的方法,如果提供了一個強類型的迭代器,生成的IL代碼就會調用這個版本的GetEnumerator,強類型自然就沒有GC的問題了。所以現在的C#里用foreach是沒問題的,但是自己實現集合類型的時候記得同時實現一個強類型的IEnumerator&
而優化代碼一定要在實際環境中測量數據。
Unity的問題在於它用的是Mono 2.6,這個版本的Mono編譯器還沒有做這個優化。Unity的GC性能跟CLR的GC相比差很多,iOS上連JIT都沒有,所以這方面還是比較敏感的。
優化方法:
- 能用for就不用foreach。
- 把代碼用Visual Studio編譯成DLL。
- 用不了for就手動調用強類型版本的GetEnumerator,然後自己寫while (e.MoveNext()) ...,最後別忘記調迭代器的Dispose。
笨是笨了點,但是在Update等每幀都需要執行的關鍵代碼中可以減少大量GC Alloc,明顯改善性能。偶爾才跑一次的話,迭代器一共也分配不了多少內存,Unity GC的Heap Block Size是1KB,能見縫插針的概率還是蠻大的。
最後,官方表示Unity 5.x會修復這個問題。(感謝 @王劍飛 指正)這個實現層面上的東西只能以實測為準。尤其是Unity這種純粹把C#當腳本語言來用的場景和用C#開發的項目完全不是一回事兒。
另外,盡量使用for來代替foreach肯定是錯的,因為有些容器說不定按照索引來訪問要比枚舉慢得多。當然話說回來,這些容器本來就不應當提供按照索引檢索的介面。
順便說一下,你貼的第二個鏈接說的顯然是錯的,那兩個循環根本不等價,因為foreach是要把值取出來的,而for循環裡面根本就沒有取值,,,還因為是ArrayList導致平白無故多了一堆拆箱的操作。
當然這和GC也沒啥關係,只是說這種光靠拍腦袋不了解原理的測試毫無意義。題主這個問題是U3D的問題而不是C#的問題好吧
在普通CLR里,針對List&
這方面x86的JIT是這樣的,x64的話,經tracing似乎還是foreach。x64運行時目前做的比較草率,似乎4.6開始(RyuJIT?)就精細很多了,因為之前不是特地寫的JIT,而是用C++後端湊合改的。
至於foreach的內存問題,首先,無論如何也不會多用幾kb的內存,幾十個位元組最多了。其次,你看內置的集合,都額外寫了一個struct的Enumerator,這樣就可以把內存分配在棧而不是堆上,避免給GC造成壓力(包括不用LINQ)。一般來說,對於需要密集foreach的情況,都是建議這麼做的。
至於Unity,估計對性能要求會比較高,所以更需要注意這方面。foreach有隱含的對象引用問題,如果能替換的話,還是for比較放心。
其實現在已經懶到Foreach都不寫了
List&
linq大法好 擴展方法大法好啊
C#大部分能循環的東西都沒辦法for,只能foreach,所以你這個問題其實沒什麼意義。如果針對的是數組或列表這些標準庫類型的話,我認為編譯器應該讓for和foreach生成相同的代碼。
我有看到一些網頁上有unity的mono foreach泄露的問題,包括在4.x的issue list上有看到過。在unite 2015北京站,我有問過unity中國的人,他們並不知道這個問題。補充:unite2016上海站,承認這個問題了Unity foreach 造成額外的GC開銷
例如針對Dictionary&
void TestListForeach()
{
foreach (var i in TestContainer)
{
CountString(i.Key);
}
}
會產生額外的GC Alloc,原因是這段代碼等同於:
void TestListNoForeachWithUsing()
{
using(var i = TestContainer.GetEnumerator())
{
while(i.MoveNext())
{
CountString(i.Current.Key);
}
}
}
當using結束進行Dispose時,因為IEnumerator&
void TestListNoForeachSafe()
{
var i = TestContainer.GetEnumerator();
try
{
while(i.MoveNext())
{
CountString(i.Current.Key);
}
}
finally
{
i.Dispose();
}
}
這個寫法寫起來挺拗口的,如果不調用IEnumerator&
void TestListNoForeach()
{
var i = TestContainer.GetEnumerator();
while(i.MoveNext())
{
CountString(i.Current.Key);
}
}
那Dictionary.Dispose做了什麼呢?
// 摘自Unity Mono的 mcs/class/corlib/System.Collections.Generic/Dictionary.cs 文件
public struct Enumerator : IEnumerator &
Dictionary&
// ...
public void Dispose ()
{
dictionary = null;
}
}
通過以上代碼,我認為針對Dictionary是可以不調用Dispose的。當然作為通用流程還是要調用的,如果想採用簡單寫法,必須明白Dispose做了什麼。
結論因為foreach比其替代寫法,清晰明了太多,建議只用在一些不常調用的函數上。 可以先使用foreach開發,針對Profiler進行定點的優化, 為了項目不易出錯,改寫的代碼,採用TestListNoForeachSafe寫法。
是的,至少在bug修復前是的
然而這個問題只在mac上面解決了?Windows仍然存在?
所有Update里都禁止使用foreach,否則會頻繁垃圾回收導致卡頓不多說了,下班前得把所有foreach都改掉...
就是foreach每個遍歷都會返一個IEnumerator 對象(C Sharp 基礎問題)
如果代碼角度來說顯然是for快,foreach中間多了個代理類操作訪問.不過實際測試可能不明顯,由於一系列的影響可能些結果是foreach會快.
你真的已經喪心病狂到連這40B也要省了么??
以下是從執行時間上來看哪個效率高
可以看出foreach比for用的時間多那麼一些,但是
我把for放到前面,那麼for的時間比foreach多,執行時間幾乎是相同的,所以..效率上沒啥差別,測試環境是.NET Framework 2.0 控制台程序
性能什麼的,應該不用考慮吧,等到慢了再說。
推薦閱讀:
※unity3d怎麼用代碼實現縮放粒子特效?
※由unity引擎做的遊戲《缺氧》,它的地圖的機制是怎麼實現的?
※unity中從Resources下讀取較大的資源會卡,有解決辦法么?
※如何評價雲風與xlua作者關於unity,c#,lua的討論,以及他後來給出的方案?
※mipmap的優點具體是什麼?