[筆記] Review of Unite Europe 2017 – Squeezing Unity
這是看 Unity 官方 Youtube 頻道的影片後,留下來的筆記。
相關連結:
- 影片 – Unite Europe 2017 – Squeezing Unity: Tips for raising performance
- 投影片 – Unite Amsterdam 2017 – Squeezing Unity
主題大綱 – 提升 Unity 效能的技巧
- 較不良的 Unity API
- 資料結構
- 行內 (inline) 方法
- Unity UI
筆記內容
講者講在最前頭的:(唉?!)
Particle System
- Start、Stop 等諸多 APIs 預設會對 所有的子物件 作遞迴呼叫,假如有 100 個子物件,就會導致 100 次 GetComponent,相當傷害效能。建議永遠傳入參數 withChildren = false;如果有子系統需要控制,改用事先自行 Cache 所有粒子系統並手動控制。
- Stop 跟 Simulate 兩個 API 在呼叫的時候都會有嚴重的 GC 問題,開發者無法自行解決。這問題在 2017.2 將會修正。
Array-returning API
- 為了資料結構的安全考量,所有回傳 Array 的 API 方法或 getter 欄位都會 產生一份新的 copy,考量記憶體配置,這類 API 盡可能減少使用。如:GetComponents、Input.Touches。
- Input.Touches 應該用 Input.touchCount 與 Input.GetTouch 取代。
- Physics 底下的 xxxCastAll 也因類似原因建議避開,改用新的 NonAlloc 版本。
資料結構
- 常見的資料結構中,Array 跟 List 適合遍歷 (iteration),Dictionary 跟 HashSet 適合做 index/key 的查詢;反過來當然不建議用 Dictionary 做遍歷。
- 但如果需要遍歷也需要查詢時?兩個都用。同時維護兩組資料結構,以記憶體空間換取運算時間。
- 一般比較 GameObject 是否為同一物件時,C# 會比照一般類別去比較最底層的實體配置,如果要進一步優化,可以透過 Unity 的 Instance Id 來比較。
- Object.GetInstanceID 本身需要時間,因此這個優化手段要配合 Cached Instance Id 才會明顯。另外規模不大的比較也別做這件事了。
行內方法 (inline method)
- 行內方法可以優化方法呼叫的時間,C# 有 AggressiveInliningAttribute 可以使用,不過目前這在 Unity 不支援 IL2CPP (筆者個人覺得既然如此還是就先別用了)
- 同樣的理由,把不必要的 property getter/setter 封裝改成 public 成員也會有優化效果。
Unity UI
接下來的 Unity UI,佔了整個講座一半。這邊內容很多,加上把講者的話轉譯過來後,沒有特別實驗確認細節,如果謬誤麻煩告訴我。
Unity UI 相關的優化 – Rebuild
- UI 以 Canvas 為單位,有兩個重大的運算:Draw 跟 Generate Mesh。
- Draw 固定每個 Frame 運作一次。
- Generate Mesh 會在底下元件有改變時運算,為優化重點。
- Generate Mesh 的動作在 UI 稱為 Rebuild,過程大致如下:
- 計算 Auto layout
- 重建 所有 enable 的元件對應 mesh (即使 Alpha = 0 也會重建)
- 準備 Batch 所需要的 Material
- 依照深度進行排序 (所有 UI 元件都是透明渲染,所以這步驟很重要)
- Batch Mesh
- Rebuild 過程中,深度排序因為排序演算法的極限,子物件越多,越會以 超過線性遞增 的速度增加消耗。
- 為了減少 Rebuild 次數,建議將 UI 切成多個 Canvas,讓動態元件跟靜態元件分開,變動頻率接近的 UI 為一組,節省部分元件的重建次數。這個動作同時也會破壞 UI 的 Batch,兩者要視情況尋求平衡。
- 另外減少 Rebuild 次數的手段,是控制 Input 對 UI 的影響,過小的變動則過濾掉。比如放任原生的 Scroll View 運作,最後幾個 Frame 可能會只剩下 0.0001 的位移,是完全不必要的 Rebuild。
Unity UI 相關的優化 – Graphic Raycaster
- Graphic Raycaster 每個 Canvas 都會有一個,用於接受輸入訊號,判斷該傳遞何事件給予 UI 元件。
- 如果 UI 元件本身沒有互動的需要,可以關閉 raycast target,節省 Raycaster 檢查的元件數量。
- World Space 的 Canvas 會有個 Event Camera 用於 Graphic Raycaster,如果這個位置留下 null,Graphic Raycaster 會每個 Frame 自動用 Camera.main 補上。
- Camera.main 本身是透過 GameObject.FindWithTag 運作,每次呼叫都會重新執行,所以不論如何,Event Camera 不要留白。
- Camera.main 不只在 UI,整個專案都應該盡量避免呼叫,當場景中有越多 Tagged GameObject,消耗就越大。
- 如果整個 Canvas 都不需要有玩家互動,直接關閉 Graphic Raycaster。
Unity UI 相關的優化 – SetDirty
- UI 的 Auto Layout 是否重新計算,取決於 LayoutElement.SetDirty 是否觸發,SetDirty 本身就會造成效能瓶頸。
- UI LayoutGroup 每次 SetDirty 都會把所有 parent 一起處理過,有時會在一個 Frame 多執行到 7.8 次,所以巢狀 LayoutGroup 很傷效能,LayoutGroup 應該盡量避免,或用 RectTransform.Anchor 來代替。
- Layout SetDirty 的觸發來自於元件 Enable/Disable、Reparant、Property 變動等,基本上可以當作有操作變化就會觸發。
- Canvas 本身的 Enable/Disable 不會觸發 SetDirty,所以是用來隱藏整組 UI 的好手段,只是因為沒有關閉 GameObject 本身,所以子物件有的 Update 等方法也要自行追蹤並手動開關。
- Animator 不論是否有 Animation 的變化,都會每個 Frame 觸發 SetDirty,所以除非確定有如此高頻率的動畫需要,應該避開使用 Animator 在 UI 上。
後記
這段講座沒有對應完整的投影片,只有其中的部分內容,但已經是滿滿的資訊量噴湧而出了,是相當值得觀看的一場講座。
其中有提到幾個可以透過呼叫、修改底層 dll 的優化,因為相對極端,就沒有筆記下來。