2014年3月9日 星期日

[翻譯]Unity, C#, .Net Mono的記憶體節省方法

[翻譯]Unity, C#, .Net Mono的記憶體節省方法

這篇斷斷續續混了好久才翻完....

==以下為翻譯內文=================================================
iOS上的Unity使用非常早期的Mono heap記憶體管理機制。這個機制不包裝中介層,所以一旦你讓heap碎片化,它會直接抓一塊新的記憶體給你。我的印象中Unity的實驗室怪傑們在做一個新的heap管理機制來處理這個問題,但現在即使是一個沒有memory leak的遊戲也會因為不斷增加的記憶體數量而耗盡記憶體。

C#是個有趣的程式語言,可讓你在不犧牲可讀性的狀況下快速寫出功能強大的的程式碼。然而事情的另一面是撰寫C#程式碼會自然的生出許多的garbage collection,解決掉這問題的唯一方法就是消除或是減少heap allocation。我在不減少功能性的情況下列了個簡單的解決方案清單。

最終的效果就是你的C#程式碼會看起來更像是C++,而且你將失去一些C#所帶來的威力,不過這就是人生。作為獎勵,heap的alloc在CPU上會比stack alloc更加集中(應該是指不會那麼頻繁alloc),所以你應該可以節省下一些frame time。

要標定記憶體削減目標時,Unity profiler可以幫你找到哪些function做了很多allocation。它並沒有提供很多資訊,但至少有。打開Profiler後啟動你的遊戲,選擇CPU profiler並點選GC Alloc欄位,這樣可以依照GC狀況的糟糕程度來排出先後。先使用下列的方針來處理這些function:

  • 避免使用foreach()。這會在你的list type中呼叫GetEnumerator(),進而allocate一個很快就會被殺掉的enumerator在heap中,你得改用更囉唆的c++ for(;;)語法才行。
  • 避免使用string。string在.NET中是建立在heap中且長度不可變的。你無法像c語言的版本一樣操作string。就UI而言,使用StringBuilders來建立一個string會是個對記憶體較有效率的作法,它會延遲轉換成string的時機直到最後需要的時候才做。你依然可以用string作為keys(應該是指Directory的key),因為literals應該要指到記憶體中的同一個instance,但是別太過度去操作string。
  • 使用struct。Struct型別在mono是allocate在stack中,所以當你有個工具類別不想要放掉,用struct。記住struct是傳值(而不是傳reference),使用到struct時可以加個ref的修飾字在函式參數前以避免copy的效能損耗。
  • 用structs取代固定大小且在scope內array。如果你有個固定大小而且在scope內的array(像是只在某function內存在),考慮建立一個member array(class內)或是一個struct來代替(或暫存)它。我曾經將每個frame中呼叫spline class都要建立的Vector3[4]改成用一個有四個欄位的ControlList struct代替。我只需要多加一個[]屬性來做為index存取用,而這省下了大量使用該函式而產生的allocation。(註,簡單說來就是用多一些stack或是事先建立好暫存的資料來避免動態allocation)
  • 填入ref傳入的lists優先於回傳一個新的lists。傳進去時我們還是需要heap-alloc一個list,所以聽來像是沒有省下什麼東西?但是這讓我們可以使用下一個最佳化的骯髒手段。
  • 考慮暫存一個常使用的function內的資料為class member。如果你有個funciton每次呼叫時都要用到很大的list,把那個list存成class member讓該資料空間可以一直保存下來。在C#中呼叫.Clear()並不會清掉buffer,所以在下一個frame時我們幾乎或不會有任何的allocs。這看起來很髒也讓程式碼不好讀,但是可以產生很大的效能差異。
    (註,一樣是用事先暫存的手段)
  • 避免IEnumerable extension methods。不用說,大多的Linq IEnumerable extension methods就如同他們有多好用一樣的也同樣會建立一些allocations。然而令我驚訝的是,在IList<>中呼叫.Any()應該只是呼叫一個Count>0的虛擬函式計算,在這裡卻會觸發一個allocation。其他在IList<>中微不足道的操作在IEnumerable中都會有同樣狀況,像是First()以及Last()。如果有人可以告訴我原因的話我會很感激的。基於如此以及foreach()的問題,我目前會說避免使用IEnumerable abstraction作為interface,改用IList<>。
  • 盡量減少使用function pointer。將一個類別方法放到delegate或是一個Func<>中會導致box(轉型成c#物件的動作,http://msdn.microsoft.com/zh-tw/library/25z57t8s(v=vs.80).aspx),而這會觸發一次記憶體配置。我找不到可以儲存一個method link而不觸發boxing的方法。我留下了大部分的function pointer,因為它在解耦物件關係上有極大幫助,所以對我來說還好,但我也處理掉了一些。
  • 小心複製material。如果你從任何的renderer中取得material property,這個material會在你即使沒做任何事的狀況下也會clone一份。這個clone出來的material不會被GC掉,除非你換了關卡或是呼叫了Resources.UnloadUnusedAssets(),否則它不會被清掉。如果你沒有要改動該material,改用myRenderer.sharedMaterial。
歡迎各位給予指教。