哲人告訴我們:勿過早最佳化
在編寫C#程式碼的時候,開發者會發現:使用C#的foreach迴圈的效能會比對應的for迴圈要稍微慢一些。
foreach迴圈結構
for迴圈結構
我想說的第一件事是:這個效能差異,實在是太微小了,以至於可以完全忽略掉。可千萬別有這個想法:我如果把程式碼中的所有foreach迴圈改寫為對應的for迴圈,程式的效能應該可以大大提升。這是不會發生的,因為迴圈的開銷很少會出現在非基準程式(non-benchmark)花費大部分時間的地方。
今天我要說的主題不是如何透過放棄foreach迴圈來提高程式碼執行效能。我今天的主題是回答這個問題:”為什麼編譯器不會將foreach自動轉換為相應的for,這樣程式碼仍然是可讀的,而且還可以利提升效能。”
原因是兩個迴圈的本質並不相同。
列舉的語義是:不允許在進行列舉時更改要列舉的物件。如果你這樣做,則列舉器將在下次相關呼叫時丟擲InvalidOperationException異常。另一方面,在for迴圈中,你可以隨意更改集合,這個是允許的。如果將專案插入到for迴圈內的集合中,則迴圈將繼續進行,並且取決於插入發生的位置,你可能會對專案進行兩次列舉。
如果編譯器將foreach更改為for,則以前會引發異常的程式現在可以正常執行。你是否認為這是一項”改進”呢?(根據實際應用場景,可能使程式崩潰比產生不正確的結果更好。)
現在,編譯器也許能夠分析出你沒有在迴圈內更改集合,但這項分析通常很難。例如,下面的迴圈程式碼會更改集合嗎?
看起來上面的程式碼並沒有改變集合。但是誰知道呢,萬一這個target類似於如下的物件呢:
啊哦,你可能根本不知道o.GetHashCode()還會修改內部的ArrayList。因為它看起來是如此”無害”的操作啊!
如果SneakyContainer類是來自另一個程式集,則編譯器必須假設最壞的情況,因為編譯器根本不知道外部程式集的內部實現方法。
如果你覺得這還不夠混亂的話,那麼還有另一個例子。ArrayList類未宣告為sealed。因此,有人可以重寫其IEnumerable.GetEnumerator並返回非標準的列舉器。例如,這是一個始終返回空列舉器的類:
你可能覺得:誰會那麼無聊會重寫列舉器呢?好吧,這是一件很奇怪的事情,但是更普遍的是,開發者可以重寫列舉器,以便新增過濾器或更改列舉的順序。
因此,你甚至無法相信ArrayList確實是ArrayList,因為它內部可能是一個空的列舉器(ApparentlyEmptyArrayList)。
現在,如果編譯器想要執行此最佳化,則不僅要證明列舉的物件未在列舉內部進行修改,還必須證明該物件確實是ArrayList而不是可能具有重寫了GetEnumerator方法。
鑑於交叉彙編類的後期繫結性質,編譯器可以證明這些要求的情況的數量確實非常有限,以至於不太可能在不更改語義的情況下安全地執行程式碼最佳化。
總結我對C#不是很熟悉,我的建議是:始終使用一種你最為熟悉的語法結構,並保持一致。例如,總是使用if/else而不是switch,if語句總是新增大括號,比較表示式始終用括號括起來等。這樣在寫程式碼的時候,就會形成肌肉記憶,你都還沒思考,程式碼就寫出來了。腦細胞活力MAX。
最後Raymond Chen的《The Old New Thing》是我非常喜歡的部落格之一,裡面有很多關於Windows的小知識,對於廣大Windows平臺開發者來說,確實十分有幫助。本文來自:《Why the compiler can’t autoconvert foreach to for》