設定每幀時間預算
幀率(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時間資訊。