Unity性能分析(二)CPU/GPU分析

設置每幀時間預算

幀率(fps)並不是衡量遊戲穩定體驗的理想指標。考慮以下情況:在運行時的前0.75s內渲染了59幀。然後接下來的1幀需要0.25s才能渲染完畢。雖然是60fps,但實際上會讓玩家感覺卡頓。

這是需要設置幀時間預算的重要原因之一。這爲您提供了一個目標,在對遊戲進行分析和優化時可以朝着這個目標努力,最終創造更流暢、更穩定的遊戲體驗。

基於目標fps,每幀都將有一個時間預算。一個目標30fps的應用程序每幀時間預算不應超過33.33ms(1000ms/30fps);同理,目標60fps分配給每幀的時間預算爲16.66ms。

在非交互式情況(例如顯示UI菜單或場景加載)中,可以超過這個時間預算,但在遊戲玩法過程中不行。即使只有一幀的時間超過了預算,也會導致卡頓。

在VR遊戲中,始終保持高幀率非常重要,這樣才能避免給玩家造成不適。

FPS:具有欺騙性的指標

遊戲玩家常用的衡量性能的方法是幀率(fps)。然而建議改用幀時間。請看下面這幅以fps和幀時間爲變量的圖表。

fps vs. frame time

考慮以下數字:

1000ms/900幀=每幀1.111ms

1000ms/450幀=每幀2.222ms

1000ms/60幀=每幀16.666ms

1000ms/56.25幀=每幀17.777ms

如果應用程序以900fps運行,這意味着每幀的幀時間爲1.111ms。在450fps時,每幀的幀時間爲2.222ms。這表示,即使幀速率下降了一半,每幀的差別也僅爲1.111ms。

如果比較60fps和56.25fps之間的差異,那麼每幀的幀時間分別爲16.666ms和17.777ms。同樣這也表示每幀多了1.111ms的時間,但在這裏,幀速率下降在百分比上感覺要小得多。

這就是爲什麼開發人員使用平均幀時間來衡量遊戲速度,而不是使用fps。別擔心fps,除非幀率掉到了目標幀率之下。

移動端挑戰:發熱管理和電池續航

發熱管理是移動端開發的重要優化方向之一。如果CPU或GPU由於低效的代碼而一直保持滿負荷,會產生芯片發熱問題。爲了避免芯片受損,操作系統將降低設備的時鐘速度以降溫,會導致幀率卡頓和用戶體驗下降。同時移動設備發熱也會影響電池壽命。

高幀率和增加代碼執行(或DRAM訪問操作)會導致更大的電量消耗和發熱。糟糕的性能還可能直接排除了低端設備,這可能會導致錯失市場機會。

在解決發熱問題時,要考慮到全局預算來解決問題。通過使用早期分析技術來優化遊戲,爲目標硬件配置項目設置,以應對發熱和電池問題。

調整移動設備的幀時間預算

爲了延長遊戲可玩時長,並解決發熱問題,通常建議每幀保留約35%的空閒時間。這給移動芯片提供了降溫時間,並有助於防止過度耗電。設定目標幀時間爲33.33ms(30fps),設備的幀時間預算將約爲22ms。

公式如下:(1000ms/30)* 0.65 = 21.66ms

要達到60fps,使用上面的公式得出(1000ms/60)* 0.65 = 10.83ms。這在許多移動設備上很難實現,並且會使耗電速度2倍於30fps時。因此,多數移動遊戲的目標幀率選擇30fps而不是60fps。使用Application.targetFrameRate來設置幀率。

在性能分析時,移動芯片的頻率縮放可能會影響識別幀空閒時間。在優化之前和優化之後,使用自定義工具(如FTrace或Perfetto),來監測移動芯片的頻率、空閒時間和頻率調節。

只要保持在目標幀時間預算內(30 fps爲33.33ms),並且幀率和設備溫度都很穩定,那麼就沒什麼問題。

使用FTrace或Perfetto等工具監視CPU頻率和空閒狀態,以幫助識別幀預算優化的結果

在移動設備上,每幀分配空閒時間的另一個原因是考慮到現實中的溫度變化。在炎熱的天氣裏,移動設備的發熱和散熱問題會加重,將會導致遊戲性能下降。留出一定比例的幀預算將有助於避免這些情況。

減少內存訪問操作

在移動設備上,DRAM訪問是一種耗能操作。optimization advice for graphics content on mobile devices指出,LPDDR4內存訪問成本約爲每字節100皮焦耳。

通過以下方式減少內存訪問:

  • 降低幀率
  • 在允許的情況下降低顯示分辨率
  • 使用頂點數量較少和屬性精度較低的網格
  • 使用紋理壓縮和多級紋理映射技術

當需要專注於Arm或Arm Mali設備時,Arm Mobile Studio(特別是Streamline Performance Analyzer)等工具,可用於識別內存帶寬問題。這些工具針對每個Arm GPU代進行了列出和解釋,如Mali-G78。請注意,Mobile Studio GPU分析依賴Arm Mali。

Arm的Streamline Performance Analyzer包含大量性能計數信息,可以在目標Arm硬件上進行實時分析時捕獲該信息。有助於識別由overdraw引起的內存帶寬飽和等性能問題。

爲基準測試建立硬件分級

在不同的平臺下,還需要爲設備做檔位分級,並分別確定一個最低規格設備,並做針對性性能分析和優化。 例如,在移動平臺下支持三個檔位,基於目標硬件做品質控制(啓用或關閉一些特性)。然後針對各級別中的最低規格設備進行優化。

從高到低級別的性能分析

在性能分析時(禁用Deep Profiling),使用自頂向下的方法收集數據並記錄哪些情況會導致核心循環中出現不必要的託管分配或太多的CPU時間。

首先需要收集GC.Alloc標記的調用堆棧。

如果報告的調用堆棧詳情不足以跟蹤分配源,那麼啓用Deep Profiling進行第二次性能分析,以查找分配源。

早期性能分析

在項目早期階段開始性能分析可以獲得最佳的優化效果。在項目早期,定期進行性能分析,以便您和團隊瞭解項目的性能水平。如果性能出現急劇下降,就能夠輕鬆地發現並解決問題。在目標設備上運行遊戲,同時利用平臺特定的工具進行性能分析,以獲得最準確的分析結果。

找出瓶頸

在一些平臺上,很容易確定您的應用程序是由CPU或GPU限制。例如,從Xcode運行iOS遊戲時,幀率面板顯示了一個柱狀圖,其中包括CPU和GPU的總時間,可以看到對比。注意,CPU時間包括等待VSync(移動設備上始終是啓用的)的時間。

Xcode fps視圖,顯示了遊戲運行時,CPU和GPU都運行在33.3ms內。

什麼是VSync?

VSync將應用程序的幀率與顯示器的刷新速率同步。這意味着,如果您有一個60Hz的顯示器,並且遊戲的幀預算在16.66ms內,則它會強制以60fps運行,而不允許更快。將幀率與顯示器的刷新速率同步,可以減輕GPU的負擔並解決屏幕撕裂等視覺圖像瑕疵。在Unity中,通過Quality settings 可以設置VSync Count (Edit > Project Settings > Quality)。

Unity Profiler提供了足夠的信息來定位性能瓶頸。下面的流程圖說明了初始的分析過程,後面的部分提供了每個步驟的詳細信息。

爲了全面瞭解所有CPU活動,包括等待GPU時的情況,可以使用Profiler CPU usage模塊中的timeline視圖。熟悉常見的Profiler marker以幫助正確理解捕獲結果。一些Profiler marker可能因目標平臺而異,因此花時間在每個目標平臺上瀏覽捕獲結果,瞭解“正常”捕獲結果的特徵。

項目的性能受限於芯片或線程中最耗時的部分。優化工作也應該集中在這些部分。假設遊戲的目標幀時間預算爲33.33ms,並啓用了VSync:

  • 如果CPU幀時間(不包括VSync)爲25ms,GPU時間爲20ms,那就沒有問題了!雖然受限於CPU,但時間在預算內,優化也不會再提高幀率(除非將CPU和GPU都降到16.66ms以下,並提高到60 fps)。
  • 如果CPU幀時間爲40ms,GPU爲20ms,這時受限於CPU,並需要優化CPU性能。優化GPU性能沒有任何幫助,可以將一些CPU工作轉移到GPU上,例如使用計算着色器而不是C#代碼,以平衡出其差異。
  • 如果CPU幀時間爲20ms,GPU爲40ms,這時受限於GPU,需要優化GPU工作。
  • 如果CPU和GPU都達到了40ms,那麼受限於兩者,需要將它們都優化到33.33ms以下才能達到30 fps。

是否在幀預算內?

在開發中定期進行分析和優化,以確保CPU線程和整體GPU幀時間都在幀預算內。

下圖是一款移動遊戲的分析捕獲圖像,該遊戲在高配手機上達到60 fps,在中/低配手機上達到30 fps。

該遊戲在不超過22毫秒的幀預算內,以30 fps流暢運行且不會過熱。直到VSync,主線程的WaitForTargetfps會填充主線程時間,而渲染線程和工作線程中還有灰色的空閒時間。同時,可以通過查看Gfx.Present幀結束時間來觀察VBlank間隔。

注意到當前幀的近一半時間都由黃色的WaitForTargetfps Profiler標記佔據。應用程序設置Application.targetFrameRate爲30 fps,並且啓用了VSync。主線程上的實際處理工作在約19ms,其餘時間花在等待,然後開始下一幀。

標記在不同平臺或禁用VSync時可能不同。重要的是檢查主線程是否控制在幀預算時間內運行,或者顯示有某種標記,代表主線程正在處於等待VSync或者其他線程的空閒時間內。

空閒時間由灰色或黃色的標記表示。上圖中顯示,渲染線程正處於Gfx.WaitForGfxCommandsFromMainThread的空閒狀態,這表明它已經完成了一幀中對GPU的draw call發送,並正在等待下一幀中來自CPU的draw call請求。同樣,雖然Job Worker 0線程在Canvas.GeometryJob中花費了一些時間,但大部分時間是空閒的。這些代表應用程序在幀預算內流暢運行。

CPU受限

如果CPU超出了幀預算時間,下一步是調查哪個線程最繁忙。分析找出瓶頸作爲優化的目標;如果依靠猜測,可能會優化遊戲中非瓶頸的部分,導致整體性能幾乎沒有改善。有些“優化”甚至反而會降低遊戲的整體性能。

CPU成爲瓶頸的情況相當少。現代CPU具有許多不同的核心,能夠獨立並行地執行任務。不同的線程運行在CPU核心上。Unity使用不同的線程以達到不同目標。查找性能問題的常見線程有:

  • 主線程:默認情況下,這是所有遊戲邏輯和腳本執行其工作的地方,在像物理、動畫、用戶界面和渲染等特性和系統中花費大部分時間。
  • 渲染線程:在渲染過程中,主線程檢查場景並執行相機剪裁、深度排序和draw call batching,生成需要渲染的對象列表。這個列表傳遞給渲染線程,後者將其從Unity內部的平臺無關表示轉換成特定的圖形API調用,以指示GPU在特定平臺上執行工作。
  • Job worker線程:可以使用C# job系統安排某些工作在job worker線程上運行,以分擔主線程的工作量。Unity的某些系統和特性也使用job系統,如物理、動畫和渲染等。

主線程

下圖顯示了一個主線程受限的情況。

主線程受限的項目中捕獲的結果

即使考慮到幀末段的少量分析器開銷,主線程也佔用了超過45ms,這意味着幀率不到22fps。這裏沒有顯示主線程等待VSync的空閒時間的標記;主線程整個幀期間都處於工作狀態。

下一步是確定當前幀中佔用時間最長的部分,並瞭解原因。當前幀中,PostLateUpdate.FinishFrameRendering佔用了16.23ms,超過整個幀率預算時間。檢查發現,有5個名爲Inl_RenderCameraStack標記的實例,表明有5個處於活動狀態的相機在渲染場景。Unity中每個相機都會調用整個渲染管道,包括剔除、排序和批量處理,因此當下最優先的任務是減少活動相機的數量,最好只保留一個活動相機。

BehaviourUpdate標記(表示所有MonoBehaviour Update()),佔用了7.27ms,同時timeline中品紅色部分表示腳本中分配託管堆內存的位置。切換到Hierarchy視圖,在搜索欄中輸入GC.Alloc進行過濾,可以看到在當前幀中分配內存佔用約0.33ms。但是,這不是衡量內存分配對CPU性能影響的準確方法。

GC.Alloc標記實際上不是通過測量開始到結束點的時間來計時的。爲了降低開銷,它們只記錄開始的時間戳加上分配的大小。爲確保它們可見,Profiler會爲它們分配一小部分時間。實際上分配可能需要更長的時間,特別是需要從系統申請新的內存時。爲了清晰地看到影響,可以在對應的代碼周圍打上Profiler標記,在深度分析中,timeline視圖中品紅色GC.Alloc採樣之間的間隔,指示了它們可能的消耗時長。

此外,分配新內存可能對性能產生負面影響,這些影響更難以直接測量:

  • 從系統請求新內存可能會影響移動設備上的電源,導致系統降低CPU或GPU的運行速度。
  • 新內存可能需要加載到CPU的L1緩存中,從而推出現有的緩存行。
  • 當託管內存中的可用空間不足時,可能直接或延遲觸發GC。

在當前幀開始時,4個 Physics.FixedUpdate 實例佔用了 4.57ms。隨後,LateBehaviourUpdate標記(MonoBehaviour.LateUpdate())佔用了 4 ms, Animator 大約佔用 1 ms。

爲了項目達到預期幀率,需要調查主線程的所有問題並找到適當的優化方法。通過優化時間佔比最長的部分來實現最大的性能提升。

以下是主線程受限時,查找問題容易獲益的地方:

  • 物理
  • MonoBehaviour 腳本更新
  • 垃圾分配和回收
  • 相機剔除和渲染
  • draw call batching問題
  • UI 更新、佈局和重建
  • 動畫

針對具體問題,使用其他工具:

  • 對於 MonoBehaivour 腳本,可以在代碼中添加 Profiler 標記或啓用深度分析。
  • 對於分配託管內存的腳本,啓用 Allocation Call Stacks 定位分配來源。也可以啓用深度分析或使用 Project Auditor。
  • 使用 Frame Debugger 來調查draw call batching。

渲染線程

以下顯示了渲染線程受限的情況。其目標幀預算爲 33.33 ms。

profiler顯示,在當前幀開始渲染之前,主線程在等待渲染線程(Gfx.WaitForPresentOnGfxThread 標記)。渲染線程仍在提交上一幀的draw call命令,並且還沒有準備好接受主線程的新draw calls;渲染線程中Camera.Render 正在耗時。

可以通過標記的顏色區分當前幀標記和其他幀標記,後者顏色更暗。還可以看到,一旦主線程能夠繼續發出draw call給渲染線程,渲染線程需要超過 100 ms的時間來處理當前幀,這也給下一幀製造了瓶頸。

進一步的調查發現,該遊戲有一個複雜的渲染設置,涉及9個相機和許多由替換着色器引起的額外pass。使用前向渲染路徑渲染超過 130 個點光源,每個光源可以增加多個附加的透明draw call。這些問題合在一起,每幀會產生超過 3000 次draw call。

以下是常見的導致渲染線程受限的原因,需要進一步排查:

  • draw call batching問題,特別是在舊的圖形 API上(如 OpenGL 或 DirectX 11)。
  • 相機過多。除非製作的是分屏多人遊戲,一般只需要一個活動相機。
  • 剔除問題,導致渲染物體過多。調查相機的截錐體大小和剔除層掩碼。考慮啓用遮擋剔除,甚至創建自定義遮擋剔除系統。查看場景中有多少投射陰影的對象 - 陰影剔除與“常規”剔除是在不同的通道中進行的。

Rendering profiler顯示每幀draw call batches和 SetPass call數量的概述。查看draw call batches的最佳工具是 Frame Debugger。

GPU受限

如果主線程在Profiler標記(例如Gfx.WaitForPresentOnGfxThread)中花費大量時間,而渲染線程同時顯示Gfx.PresentFrame或<GraphicsAPIName>.WaitForLastPresent等標記,則應用程序出現了GPU受限。

下圖捕獲自三星Galaxy S7(Vulkan)。儘管Gfx.PresentFrame中的一些時間可能與等待VSync有關,但此Profiler標記的長度表明大部分時間都在等待GPU完成上一幀的渲染。

在這個遊戲中,特定的遊戲事件觸發了使用一個着色器,將GPU渲染的draw call增加了三倍。當分析GPU性能時,需要調查以下常見問題:

  • 全屏後處理效果,包括環境光遮蔽和泛光等
  • 片元着色器:分支邏輯;使用完全浮點精度而不是半精度;過多地使用影響GPU波前佔用率的寄存器
  • 透明渲染隊列中的overdraw:低效的UI、粒子系統或後處理效果
  • 過高的屏幕分辨率,例如4K顯示器或移動設備的視網膜屏
  • 密集的網格,缺乏使用LOD
  • 緩存未命中和浪費GPU內存帶寬:由未壓縮的紋理或未啓用mipmap的高分辨率紋理引起
  • 幾何或鑲嵌着色器,如果啓用動態陰影,則可能每幀運行多次

如果懷疑GPU受限,可以使用Frame Debugger快速瞭解發送到GPU的繪製調用批次。但是此工具不能提供任何特定的GPU時間信息。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章