Introduction:
※本文不是在描述舊版本Unity中mono編譯器導致的foreach語句額外裝箱錯誤
博主是一名Unity 3D遊戲開發者,遊戲使用C#+lua開發,最近在優化C#程式碼時,發現了一處使用foreach不恰當的地方,其結果是造成了每幀近3k的GC Alloc,如此高頻率的GC堆記憶體分配,會導致垃圾回收的呼叫更加頻繁,從而影響遊戲效能,而這隻需要簡單的修改即可避免;
※使用.Net 2.0的Unity版本,如果是較新的.Net 4.x版本,由於FCL實現修改,本文中48->56
原始宣告程式碼如下:
private readonly IDictionary<int, MyClass> mDic = new Dictionary<int, MyClass>();
在每幀邏輯裡面,會多次對其進行遍歷,遍歷程式碼如下:
foreach(var keyValuePair in mDic) { //do... }
通過Unity自帶的Profiler分析,可以發現其導致的GC Alloc:
Body:
通過上面的Profiler可以發現此時foreach語句實際上呼叫的是Dictionary定義中隱式實現的IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator()方法,該方法的宣告如下:
IEnumerator<KeyValuePair<TKey, TValue>> IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator() { return new Enumerator(this, Enumerator.KeyValuePair); }
其中,Enumerator是在Dictionary類中定義的巢狀結構型別:
結構型別隱式轉換為介面型別時會發生裝箱,對於該Enumerator型別,其裝箱後大小為16(開銷位元組)+8(欄位dictionary)+8(欄位next+stamp)+16(欄位current:8(int位元組對齊)+8)=48位元組,呼叫29次即產生48*29=1392位元組的堆記憶體分配,這符合我們看到Profiler裡面看到的GC Alloc;
為了解決這個問題,只需要將變數宣告時改為Dictionary即可,不使用介面型別的變數,即:
private readonly Dictionary<int, MyClass> mDic = new Dictionary<int, MyClass>();
此時,在對mDic進行foreach迴圈時,就會呼叫Dictionary<TKey,TValue>.GetEnumerator()方法,該方法返回值型別即結構型別的Enumerator,避免了裝箱操作:
One more thing:
這裡可能很多人有個誤解,即foreach是隻能對實現了IEnumerable或IEnumerable<T>的型別物件進行遍歷,其實不然,foreach語句還可以對滿足以下條件的任何型別的物件進行遍歷:
實現了可訪問的GetEnumerator()方法,且該方法的返回值型別符合:包含可訪問的Current屬性和bool MoveNext()方法;
Conclusion:
這樣就知道了,.Net框架類庫提供的泛型集合型別都實現了這樣的方法,因此可以放心對泛型集合進行foreach遍歷,而不產生堆記憶體的分配,也因此,我們在使用這些型別時,儘量避免直接對其介面型別的變數進行遍歷;
如果您覺得閱讀本文對您有幫助,請點一下“推薦”按鈕,您的認可是我寫作的最大動力!
作者:Minotauros
出處:https://www.cnblogs.com/minotauros/
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利。