2017年3月6日 星期一

最佳實踐 - 了解Unity效能 - 分析

作者:Ian 原文

翻譯:Kelvin Lo / 海龜

Profiling - 分析 

說到優化,不得不說所有優化的源頭都是從發現問題開始,第一步是分析,根據專案技術和資源結構的分析報告結果來劃出專案問題的可能範圍。
注意:本文裡使用的一些追蹤程式碼是基於Unity 5.3版本,在未來這些功能有可能會變動。
注意:這個章節討論的原生(Native)方法(Method)名稱是由 Unity 5.3 的執行檔擷取出來的,這些方法名稱可能在未來的版本有所改變。

Tools - 工具 

關於分析,有許多能幫助 Unity 開發者分析專案的工具,而 Unity 本身有一套內建工具像是 CPU Profiler、Memory Profiler、或是 5.3 才有的 Memory Analyzer。

但是最好的分析報告通常來自於該平台的特有工具,像是:
 
  • iOS:Instruments 和 Xcode Frame Debugger 
  • Android:Snapdragon Profiler 
  • 採用 Intel CPU/GPU的平台:VTune 和 Intel GPA 
  • PS4:Razor suite 
  • Xbox:Pix tool 
這些工具大部分都能夠分析IL2CPP所包出來的C++專案,這些原生程式能執行原本在Mono下無法執行的高精度計時(high-resolution method timings)和更透明的堆疊呼叫(callstacks)。

要充分利用這些工具通常需要在這些平台上啟用 IL2CPP 然後使用轉換成 C++ 版本的專案。在原生程式碼狀態下可以透過原生工具得到完整的呼叫堆疊還有高精度計時,這些在透過 Mono 執行的時候都無法取得。
Unity 已經發表過一份關於在 iOS 平台使用 Instruments 分析的教學指南,點這裡


解析啟動軌跡 

當查看啟動時間的軌跡時,有兩個關鍵的方法來檢視,分別是從專案設定、資源和程式可能影響啟動時間的可能範圍。
請注意:啟動時間在不同的平台上會有差距,大多都是用啟動到顯示 Unity Logo(splash screen)的時間來判定。
注意 Unity 的啟動時間在不同平台上可能表現會不同,大部分的平台的行為是會讓使用者會在靜態的 Unity Logo(Splash screen)等待得時間變長。



上圖為 iOS 上用 Instruments 抓的啟動時間追蹤,可以看到 iOS 平台特有的 startUnity 方法呼叫了 UnityInitApplicationGraphics 和 UnityLoadApplication 方法。

UnityInitApplicationGraphics 執行很多內部工作,像是圖形裝置的設定工作和初始化很多Unity的內部系統。不但如此,他還負責初始化 Unity 的資源系統(Resources system)。所以它需要先載入資源系統所含的所有檔案索引。

所有目錄名為 Resources 裡面的資源檔案(在 Assets 目錄下的 Resources 目錄,還有 Resources 目錄下的目錄)都會被算在資源系統裡。因此初始化資源系統所需要的時間將會隨著 Resources 目錄裡的檔案數量增加而變久。

UnityLoadApplication 的工作包含載入和初始化專案最開始的場景。這包含反序列化(deserializing)和實例化(Instantiating)展示第一個場景需要的資料,例如編譯著色器(compiling Shaders)、上傳貼圖和實例化遊戲物件(GameObject)等等所有為了顯示初始場景所需要的資料。此外,第一個場景中所有的 MonoBehaviours 都會在這個時間點執行 Awake 回呼(Callback)。

這樣的呼叫結構代表如果你在專案的第一個場景裡的某個 Awake 回呼執行了非常耗時的程式,會拖慢整個專案的啟動時間。要解決這樣問題當然是要修改拖慢的程式碼或是移到別的地方來執行。

解析執行軌跡 啟動初始化之後主要是 PlayerLoop 的追蹤。這是 Unity 主要週期循環,裡面的程式每幀會執行一次(註一)


上圖是一個 Unity 5.4 專案的分析報告,顯示了幾個 PlayerLoop 會呼叫的最有趣的方法。請注意,PlayerLoop 裡方法的名稱可能會因不同版本的 Unity 而有所不同。

PlayerRender 是執行 Unity 渲染系統的方法。也負責剔除不顯示的物件、計算動態批次計算(Dynamic batches)和對 GPU 送出繪圖指令。所有的影像後製(Image Effects)或是基於渲染的程式回呼(例如 OnWillRenderObject)也都在此處理。一般來說,當專案跟玩家互動時 CPU 大部分時間都會花在這裡。

BaseBehaviourManager 會呼叫三種不同版本的 CommonUpdate,它們各自會呼叫場景裡被啟動(Active)的 GameObject 上的 MonoBehaviours 的特定回呼。

  • CommonUpdate<UpdateManager> 會呼叫 Update 回呼。 
  • CommonUpdate<LateUpdateManager>會呼叫 LateUpdate 回呼。 
  • CommonUpdate<FixedUpdateManager> 會呼叫 FixedUpdate(如果物理系統被觸發) 
一般來說 BaseBehaviourManager::CommonUpdate<UpdateManager> 是最有意思的方法,因為它在 Unity 專案裡大多數腳本的進入點。

還有幾個有趣的方法:

如果專案有用到 Unity UI 系統,UI::CanvasManager 會呼叫幾個不同的回呼。這行為包含了Unity UI 的批次運算和排版更新,這兩個操作最常造成 CanvasManager 出現在分析報告裡。

DelayedCallManager::Update 執行共常式(Coroutines)。在底下的 Coroutines 章節會有更詳細的說明。

PhysicsManager::FixedUpdate 執行 PhysX 物理系統。涉及到執行 PhysX 的內部程式,並受到場景內有物理行為物件的數量影響(像是帶有 Rigidbody 和各種 Collider 的物件)。然而,跟物理有關的回呼也會出現在此,特別是 OnTriggerStay 和 OnCollisionStay。

假如專案用的是2D物理(Box2D),類似的結構會出現在 Physics2DManager::FixedUpdate 之下。 



解析腳本方法

當使用 IL2CPP 跨平台編譯時,可以找看看 ScriptingInvocation 這行包含的內容,這是從 Unity 內部原生程式進入到使用者腳本的分界點。(註二) 



上圖是一個 Unity 5.4 專案的另一個分析報告,附掛在 RuntimeInvoker_Void 底下的所有方法都是交叉編譯過的 C# 腳本的一部分,每幀執行一次。

這些追蹤還蠻容易理解的,每一個命名規則都是「原始類別_原始方法」,例如上圖範例範例能找到 EventSystem.Update、PlayerShooting.Update 以及其他幾個 Update 方法。這些都是在MonoBehaviours 裡能找到的標準 Unity Update 回呼。

透過展開這些方法,可以更精確的定位誰在消耗 CPU 時間。這包含了專案的腳本方法、Unity API 和 C# 函式庫。

上面顯示出 StandaloneInputModule.Process 方法每幀會對整個 UI 進行一次 Raycasting,檢查是否有任何 UI 事件被點擊或滑過的動作觸發。主要消耗是在逐一檢查所有 UI 元素,並測試滑鼠的位置是否在其範圍內。(註三)

資源載入



資源載入也能從 CPU 追蹤裡找到,載入資源的主要方法是 SerializedFile::ReadObject,它透過名為“Transfer”的方法將檔案透過 2 進制(binary)的資料串流連接到 Unity 的序列化系統(Serialization system)。Transfer 方法可以在所有資源的載入過程中看到,例如材質、MonoBehaviours 和粒子系統。

上圖顯示出一個場景正被載入,這會讀取並反序列化(Deserialize)場景中所有資源,可以看到SerializedFile::ReadObject 之下有各種不同 Transfer 呼叫。

一般來說,如果在執行時遇到了效能問題,並追蹤看到 SerializedFile::ReadObject 用掉大量的時間,代表FPS會下降是因為資源正在載入。要注意的是絕大部分的情況下,只有當透過 SceneManager、Resources 或 AssetBundle API 請求同步(Synchronous)載入時,才會在主執行緒上看到 SerializedFile::ReadObject。

這種效能問題通常能簡單解決:可以採用非同步方式(Asynchronous)載入資源(將比較吃重的 ReadObject 呼叫放到工作執行緒(Worker thread)上),或預先載入那些較大的資源。

請注意,當複製(Cloning)物件時也會產生 Transfer 呼叫(在追蹤表的CloneObject方法裡)。假如一個Transfer 呼叫出現在 CloneObject 下就代表它不是從硬碟載入而是從一個舊的物件資料轉移到一個新的物件。Unity 做法是對舊的物件進行序列化之後將產生的資料反序列化成為新的物件。


註一:這只適用於在”Assets”底下的”Resources”目錄以及底下所有名為”Resources”的子目錄。
註二:技術上來說,執行IL2CPP之後,C#/JS腳本也會轉為原生程式(Native code)。然而,這種交叉編譯程式主要是透過IL2CPP的框架來執行,不會像手動寫程式那麼嚴謹。
註三:在Unity 5.4之前,StandaloneInputModule在沒有滑鼠的裝置上查詢滑鼠輸入存在著一些缺陷,還好後面的版本已經修復,大大降低了StandaloneInputModuleCPU消耗

Unity最佳化 - 目錄


  1. 分析
  2. 記憶體
  3. 協同
  4. 資源審查
  5. GC和Managed Heap
  6. 字串和Text
  7. Resources目錄和一般最佳化
  8. 特別最佳化

沒有留言:

張貼留言

著作人

網誌存檔