[Unity] 使用Profiler.BeginSample()定位性能熱點


Unity客戶端優化中最常使用的輔助優化工具是Profiler。使用Profiler,可以方便我們定位游戲程序的性能瓶頸,如定位游戲中單幀耗時過高的模塊、定位游戲中產生GC較多的模塊等等。

盡管如此,在實際優化分析過程中,即使直接使用Profiler定位到游戲瓶頸的大致模塊,也往往不能分析出更精確的瓶頸代碼。例如,在實際開發過程中,我們發現Game.Update()這一模塊特別耗時,其實也是無補於事。因為Game.Update()這一模塊下層可能涵蓋了網絡交互、戰斗、聊天等多個自定義模塊,如果這些模塊不是使用Unity自帶的MonoBehavior.Update實現幀循環,我們就不能往更深層次模塊進行分析了。

在默認的情況下,Profiler僅為Unity自帶的函數添加了性能采樣點(如MonoBehavior的函數、Resources庫函數等),但是Profiler無法直接對我們用戶的代碼進行采樣分析。

使用Profiler評估客戶端性能時,推薦使用Profiler提供的性能采樣接口,來更精確地分析定位客戶端存在的性能問題。

 

優點:使用Profiler提供的性能采樣接口,最大的優點是可以更深層次地分析用戶代碼的性能熱點,避免定位到大致模塊后,無法繼續往下分析,只能通過其他方式(如代碼審查)繼續優化的尷尬。

 

舉個例子,如圖:

使用BeginSample、EndSample配對,可以有效定位用戶自己編寫的代碼

 

由上圖可見,在沒有使用Profiler.BeginSample()定位的情況下,Profiler只能分析到Game.Update()模塊比較耗時,往下只能看到耗時大頭在Loading.ReadObject這一點上,但是由於邏輯無關,我們無法分析導致加載耗時的模塊具體發生在Game.Update的哪一子模塊。

而使用Profiler.BeginSample()定位后,我們可以有層次性地發現,單幀耗時過大的瓶頸耗點位於 "Game.Update() -> GameNewWork.Update() -> HandleIO"。

 

除此之外,我封裝了一套自己的接口,代碼在本文最后面。之所以封裝一層,原因如下:

1、提供Profiler性能采樣開關,可隨時關閉

2、提供字符串格式化功能,可在Profiler中顯示自定義的文本,方便定位問題(使用時需要謹慎,后文敘述

 

關於格式化字符串

有時候光知道熱點代碼位置是不夠的,還需要知道代碼中變量數據。

例如下圖OnRecv函數,代碼只是根據cmd參數,獲取已注冊的句柄,然后調用。如果沒有格式化功能,我們只能知道代碼最終執行到這一環節,而不能准確定位該句柄代表的是哪個函數。使用格式化功能,把對應句柄的函數名打印出來,我們就可以知道熱點對應的是哪一個函數。

 

慎用格式化字符串


 

以上代碼會較高頻率地觸發垃圾回收(GC.Collect()),是不是邏輯代碼有問題?

經測試發現,CurrentState.Update片段幾乎均為每幀0B的開銷,而其他代碼也不存在內存分配現象,GC.Collect()是ProfilerSample.BeginSample(format)導致的。

需要注意格式化字符串本身會帶來內存分配開銷,使用格式化字符串采樣接口時需考慮自身對代碼帶來的影響。

 

使用經驗:

1、在可能的熱點函數上插入性能采樣代碼,建議編譯手機版本來分析結果。當然,在熟悉代碼的前提下,可以方便使用PC測試分析GCAlloc等問題。原因如下:

1)PC性能相對太好,一些手機上的瓶頸函數在PC上幾乎不耗時,導致無法准確分析;

2)一些代碼,特別是插件代碼,PC和手機的執行流程不同,PC分析的結果不能准確表明手機也是同樣結果。

2、在插入性能采樣代碼時,特別留意函數中是否存在多個return的現象。這些return如果沒有處理好,就有可能導致性能采樣的Begin和End不匹配,導致Profiler顯示的結果有誤。

3、對於協程函數,BeginSample、EndSample之間注意不能存在yeild return null,否則可能導致Unity客戶端卡死、手機卡死等現象。個人分析:Begin和End配對分析的是單幀結果,出現yeild return null代表該區間將會分兩幀甚至多幀完成。

 

封裝好的性能采樣接口代碼:

 

using UnityEngine;
using System;
public class ProfilerSample {
    public static bool EnableProfilerSample = true;
    public static bool EnableFormatStringOutput = true;// 是否允許BeginSample的代碼段名字使用格式化字符串(格式化字符串本身會帶來內存開銷)
    public static void BeginSample(string name) {
#if ENABLE_PROFILER
        if(EnableProfilerSample){
           Profiler.BeginSample(name);
        }
#endif
    }
    public static void BeginSample(string formatName, params object[] args) {
#if ENABLE_PROFILER
        if(EnableProfilerSample) {
           // 必要時很有用,但string.Format本身會產生GC Alloc,需要慎用
           if (EnableFormatStringOutput)
               Profiler.BeginSample(string.Format(formatName, args));
           else
               Profiler.BeginSample(formatName);
        }
#endif
    }
    public static void EndSample() {
#if ENABLE_PROFILER
        if(EnableProfilerSample) {
           Profiler.EndSample();
        }
#endif
    }
}

 

 

閱讀更多


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM