作為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&

測試的比較簡陋,就是在Update中每次調用不同的Test函數,在Unity的profiler中看一下:TestListForeach()

TestListNoForeach()

所以簡單的測試已經有結論了:

  1. foreach會導致額外的gc對象,每次40B
  2. 這個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的結果:

可以看到這裡il2cpp是忠實的做了翻譯工作,連Box也照樣保留了下來,想來想通過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&和Array的for比foreach快,而JIT在確定這個對象是List或Array的時候,會為foreach生成優化的代碼,注意這裡不僅僅是優化成for,而是把List直接優化成對其內部數組的訪問,且不會有多餘的邊界檢查,因此可以說List&是一個特殊的存在。

這方面x86的JIT是這樣的,x64的話,經tracing似乎還是foreach。x64運行時目前做的比較草率,似乎4.6開始(RyuJIT?)就精細很多了,因為之前不是特地寫的JIT,而是用C++後端湊合改的。

至於foreach的內存問題,首先,無論如何也不會多用幾kb的內存,幾十個位元組最多了。其次,你看內置的集合,都額外寫了一個struct的Enumerator,這樣就可以把內存分配在棧而不是堆上,避免給GC造成壓力(包括不用LINQ)。一般來說,對於需要密集foreach的情況,都是建議這麼做的。

至於Unity,估計對性能要求會比較高,所以更需要注意這方面。


foreach有隱含的對象引用問題,如果能替換的話,還是for比較放心。


其實現在已經懶到Foreach都不寫了

List&.ForEach(item=&>{ 操作});

linq大法好 擴展方法大法好啊


C#大部分能循環的東西都沒辦法for,只能foreach,所以你這個問題其實沒什麼意義。如果針對的是數組或列表這些標準庫類型的話,我認為編譯器應該讓for和foreach生成相同的代碼。


我有看到一些網頁上有unity的mono foreach泄露的問題,包括在4.x的issue list上有看到過。

在unite 2015北京站,我有問過unity中國的人,他們並不知道這個問題。

補充:unite2016上海站,承認這個問題了

Unity foreach 造成額外的GC開銷

例如針對Dictionary& TestContainer進行測試:

產生額外GC Alloc的原因:

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&是結構體,進行了一次裝箱操作。

不產生額外GC Alloc的等價寫法:

void TestListNoForeachSafe()
{
var i = TestContainer.GetEnumerator();
try
{
while(i.MoveNext())
{
CountString(i.Current.Key);
}
}
finally
{
i.Dispose();
}
}

這個寫法寫起來挺拗口的,如果不調用IEnumerator&.Dispose()會好很多:

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 &, IDisposable {
Dictionary& 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的優點具體是什麼?

TAG:Unity遊戲引擎 | C# |