Unity 2017 Game Optimizaiton簡單翻譯和總結(一):定位性能問題

英語的水平有限,在閱讀時,進行了簡單的記錄和翻譯,把一些關鍵的點記錄下來,並加入了一些自己的理解和總結。

在這一章,主要探索三個問題:
1.怎樣使用unity Profiler去收集剖析數據
2.如何分析profiler數據中的性能瓶頸
3.隔離性能問題和確定根源問題的技巧

Unity Profiler
untiy Profiler在untiy編輯器中,它通過生成unity3d子系統的實時的使用和統計報告, 爲我們提供了有效的方式來縮小查找範圍。不同的子系統可以收集的數據如下所列:

  • Cpu消耗
  • 基本和詳細的渲染和GPU信息
  • 實時內存分配和總的消耗
  • Audio數據使用
  • 物體引擎(2D,3D)使用
  • 網絡消息和操作使用
  • video回放使用
  • 基本和詳細的用戶接口性能
  • 全局光照統計

當unity項目在Development 模式下編譯時, 一些附加的編譯標誌會生成一些特殊的事件,用來獲取log並保存到Profiler中。自然的, 就會引起額外的CPU和內存消耗。更壞的情況出現在untiy editor下使用profiler, 將會消耗更多的CPU和內存,比如編譯器更新接口, Scene 窗口, 處理後臺任務等,這些額外的消耗是不可忽略的。在一些超大的項目中,當開始profiler的時候,會引起不確定的行爲。當實時的深度分析代碼的行爲時是要付出代價的,我們應該時刻意識到這一點。
在進行分析前,首先進行一些簡單的測試,將遊戲運行在目標平臺上,來收集初始數據和演示一些測試場景。這個方法一般被稱爲基準法, 最重要的指標有FPS,總內存消耗, CPU的使用軌跡(一般找高峯), CPU/GPU的溫度.
在Editor下的數據不能作爲基準數據,因爲會有額外的開銷,這樣的數據可能會誤導我們, 並且隱藏一些真實環境下游戲的潛在的條件。因爲我們應該在目標平臺上運行遊戲來進行數據分析。
在Editor下的運行結果會更快, 尤其是在處理聲音文件,prefabs,腳本對象時,因爲Editor會緩存下以前導入的數據,這樣再次訪問就會更快。

在不同的平臺下連接profiler
可以參考官方手冊,有在不同的平臺的使用方法。

Profiler window基本介紹

主要分爲四大部分:
1.profiler控制面板
2.Timeline視圖
3.故障視圖控制面板
4.故障視圖
具體的介紹在官網上有介紹,這裏只介紹一些特殊的。
Deep Profiler
通常情況下profiler只記錄一般的untiy回調方法的時間和內存分配,比如Awake(),start(), update(), Fixedupdate()。使用Deep Profiler,會重新編譯我們的代碼,使profiler能夠記錄每一個調用方法。這也會導致巨大的運行開銷,使用更多的內存來存儲整個調用堆棧的數據。所以,不能用在大的項目上,可能會導致項目還沒運行unity內存就超了,建議用在小的測試場景中,而且由於需要重編代碼,在測試的時候,不要來回關閉和開啓deep profiler。
可以使用手動模式,利用Profiler.BeginSample, Profiler.EndSample, 來分析代碼區域,這樣開銷會更小。

Profile Editor
profiler editor選項,表示收集unity Editor自己的分析數據,一般用來分析enditor腳本的性能。同時connected player也要設置到Editor選項。

Connected Player
Connected player的下拉框可以選擇想要分析的目標,可以是當前的Editor, 可以是本地的遊戲的Pc包, 也可以是在遠程設備上。

Timeline View

用來展示運行時刻收集的分析數據,並在一系列的區域中顯示。每個區域都是unity引擎不同的子系統的數據,並且區域有兩部分組成,右側顯示圖形化的數據, 左側是一列類型選項, 勾選後,圖形化數據中會添加該類型的數據。當選中Timeline view的某個區域,BreakDown視圖會顯示當前幀在對應子系統中更加詳細的數據。區域可以被移除和添加;

BreakDown view
BreakDown展示的信息依賴於選擇的Timeline區域和當前的Breakdown控制面板的一些選項,下面就詳細的介紹。

CPU Usage Area
這個區域顯示了所有的CPU使用和統計數據。這個區域可能最複雜和最有用的,因爲它覆蓋了大量的unity子系統, 比如 MonoBehaviour組件,相機、一些渲染和物理過程, 用戶接口(包括Editor的接口,如果正在使用Editor運行),聲音的流程, Profiler自己等。
在展示cpu使用時,在Breakdown view可以有三種模式可以選
1.Hierarchy mode 層次結構模式
2.Raw Hierarchy Mode 原生層次結構
3.Timeline Mode 時間線視圖

層次結構模式,通過將相似的數據元素和全局unity函數調用進行分組,能夠很方便的展示調用堆棧的調用。比如,渲染分隔用到的BeginGUI(), EndGUI()調用,它們被分到一個組中;這種模式可以在最開始的時候就直觀反應出哪個函數調用佔用最多的CPU時間。
原生的層次結構,和層次結構類似,不過它沒有將全局unity函數組合在一起,而且進行隔離。這使得breakdow視圖更難觀察,不過在我們想要統計特殊的全局方法的調用次數時,就顯得更方便了。還是Begingui, endgui(), 會被分到不同的組中,可以更清晰的看到每個的調用次數。
timeline模式可能是查看CPU使用的最有效的方式, 它通過調用過程中調用堆棧的擴展和收縮來展示當前幀的CPU使用情況。breakdown的垂直部分分成不同的區域,顯示不同的線程,比如main thread, Render thread , 和各種用來處理載入場景和資源的後臺工作線程(job system)。它的水平軸代表了時間,越寬的模塊就代表了消耗更多的CPU時間, 水平的尺度也代表了相對時間, 可以方便比較兩個函數調用的CPU佔有時間。垂直軸代表了調用堆棧, 因此越深的鏈代表了此時最多的調用。
在timeline模式下,breakdown視圖的頂部模塊顯示的是untiy引擎調用的函數(start(), awake(), Update())。

GPU useage Area
和CPU使用類似,除了展示的調用和時間都是發生在GPU中。 相應的unity方法一般和相機、繪製、透明、幾何變換、光照、陰影相關的。

Rendering Area
提供一些通用的渲染統計數據,主要是CPU中發生的爲GPU渲染做的準備工作。包括setpass call數量(也叫Draw Call), 渲染場景的batches數量, 動態和靜態合批的次數,texture的內存佔用等。

Memory Area
檢查應用的內存使用情況,在breakdown視圖中,可以有兩種模式查看:

  1. Simple mode
  2. Detailed mode
    簡單模式只提供一些子系統的內存佔用的總覽數據, 包括unity引擎的底層代碼, Mono相關(總的堆尺寸,和使用的堆尺寸, 這部分內存是需要被垃圾回收的), 圖形相關的資源, 聲音資源和緩存

詳細模式展示了單個gameobject和monobehavious的原生和處理的代碼的內存佔用。不過這些信息,只有手動點擊了Take Sample時纔出現,這個是唯一的方式來收集信息。

Audio Area
聲音的統計,可以用來觀察聲音系統的CPU使用,和聲音源碼,聲音文件的內存使用情況。聲音在性能優化時經常被忽略, 但是聲音可能會變成成非常大的瓶頸, 如果沒有合適的處理,由於它潛在的硬盤訪問次數和CPU的使用,所以不要輕視它。

The Physics 3D and 2D Areas
有兩個不同的物理區域,一個是3D物理(nvidia的 PhysX),另一個是2D物理系統。這個區域聽了不同的物理統計,比如Rigidbody, Collider等。

UI and Ui detail
差的UI代碼的優化,會影響CPU或GPU, 因此我們要注意UI的代碼優化策略。

性能分析的常用方法

最好是帶着目標去分析,下面是一些checklist:
1.驗證目標腳本存在場景中
2.驗證腳本在場景中出現的次數正確
3.驗證事件的正確順序
4.減少代碼的修改。
5.減少引擎干擾
6.減少外部干擾

確認腳本存在
直接在Hierarchy中查找t:name , 將會查找所有gameobjects包含腳本或派生的腳本。

確認腳本數量
可以創建object次數過多,或者不小心實例化多個object。這些問題就導致衝突或者重複方法調用導致性能瓶頸。這種情況,我們最好寫一些初始化的代碼來阻止發生,或者寫編輯器來提醒。

確認事件執行順序
Unity幫我們處理遊戲循環,unity有固定的函數調用週期, 比如Awake(), Start(), Update, FixedUpdate()都會在特定的時間觸發。但是不同的腳本中的Awkae()的執行順序是不定的。因此我們要注意,一些初始化的過程不能放在不定順序的調用中。初始化最好放在start中, 它肯定是在Awake()中進行。不過unity了提供了手動設置腳本的執行order,就在inspector視圖中Execution order。
如果我們對直接的調用順序疑惑時,就使用deubg.log來打印出log信息作爲參考依據。不過log是開銷非常大的,所以在必要的時候使用。

協程一般會用來處理一系列事件時,它的執行依賴於yield。不過最難預估的使用可能是WaitForSeconds,unity引擎是不確定性, 每個週期都有輕微的不同,可能這一秒有60次updates, 下一秒就只有59次。在協程的開始到結束時間內,可能有不定次數的update()調用。因此在使用協程要注意,最好與其他行爲解耦。因爲這些不確定性,所以在幀同步時,要避免使用協程和mono的update, 而使用幀數來驅動update()。

儘量少的改動代碼
在分析的過程中,上面提到了,加log是一個很不錯的辦法,但是很可能會忘記移除, 就會導致最後調試時佔用更多的內存和CPU。所以要利用源碼管理工具,svn,或者git, 能夠知道每次我們的代碼修改記錄。而且要充分使用斷點調試,不用添加冗餘代碼,能夠跟蹤堆棧,變量數據和條件判斷。不過在一些必要情況下,還是要加一些條件判斷語句,來方便調試。

減少引擎自帶特性的干擾
1.經常出現的錯誤,當我們打開profiler,並使用鍵盤來操作遊戲時, 在用鍵盤觸發時,忘記點擊並返回到Editor的Game窗口。如果Profiler是最近點擊的窗口, 編輯器會發送鍵盤事件到profiler,而不是運行的遊戲。這樣就收集不到信息。如果Game Window在Editor沒有激活,沒有什麼任何東西在game窗口渲染,那麼依賴於game window渲染的時間將不會被激活。
2.VSync 垂直同步, 用來匹配遊戲幀率與屏幕刷新速度, 垂直同步會影響遊戲幀率,並且它的影響會顯示在profiler窗口上。會在層次結構中看到WaitForTargetFPS佔用很多的CPU時間。所以我們要先排除掉垂直同步,一種就是在cpu分析器中,去掉Vsync的選中框; 我們可以屏蔽掉垂直同步, Edit->Project setting ->Quality ->Dont sync。不過不能在所有平臺上都屏蔽。
3.確保Editor的Console的大量log, error, warining不會直接導致性能下降。它們會佔有大量的CPU和堆棧,會引起GC。

減少外部干擾
這一條簡單但是非常必要, 我們要檢查後臺的應用,是否佔有大量的CPU和內存。如果我們的遊戲突然性能差到超出預期,檢查系統的任務,看一下CPU,內存,硬盤任務,可能就發現問題了。


如果使用上面的checklist還沒有解決性能問題,我們可能需要更深入的分析。爲了分析我們代碼中的目標區域,提供了兩個策略:
腳本控制Profiler
Profiler可以通過Profiler類來控制,這裏主要介紹最重要的兩個分隔方法,BeginSample(), EndSample() ,在BeingSample()中可以自定義代碼的名字, 這樣就可以在層次結構中找到它。要注意的時,它只要development模式下才會編譯。

一般的CPU分析方法
Profiler只是一個分析的工具,但是我們以後可能使用其他的引擎工作,所以有必要掌握一些獨立的分析代碼的技巧。
當分析CPU使用時, 我們真正需要的就是準備的計時系統,剛好.NET庫提供了Stopwatch類(system.Diagnostics),我們可以start, stop一個stopwatch對象在任何時刻, 可以輕鬆獲取到從start開始到現在經過的時間。但是,這個類不是非常準備,所以我們可以利用多次測試平均結果的方法來減少誤差。

 public class CustomTimer  : IDisposable {
    private string m_TimerName;
    private int m_numTest;
    private Stopwatch m_watch;
    public CustomTimer(string name, int num)
    {
       m_TimerName = name;
        m_numTest = num;
        if (m_numTest == 0)
            m_numTest = 1;
        m_watch = Stopwatch.StartNew();
    }

      public void Dispose()
    {
        m_watch.Stop();
        float ms = m_watch.ElapsedMilliseconds;
        UnityEngine.Debug.Log(string.Format("{0} finished : {1:0.00}" + "ms totaol, 
{2:0.000000} ms per-test" + "for {3} tests", m_TimerName, ms, ms / m_numTest, 
m_numTest));
    }
}

使用方法:
 using (new CustomTimer("my test", num))
        {
            for (int i = 0; i < num; i++)
                TestFunction();
        }

注意點:
如果重複的內存訪問,將會造成CPU在內存中查找數據更快,因爲最近訪問過同樣的區域,所以平均時間要比實際的更少。
using方法,是一個典型的使用去保證資源在超出生命週期後合理的被銷燬。當使用using代碼塊最後,會自動調用對象的Dispose()方法,需要腳本需要繼承IDisposable接口。
通過using和CustomTimer類,我們可以在任何的引擎下來測試我們的代碼。
另一個需要關心的問題是應用的啓動時間,unity在場景啓動時需要時間去從硬盤中載入數據, 初始化複雜的子系統,比如物理和渲染系統。這些可能只需要幾秒鐘,但是如果測試代碼從初始化就開始測試,就會對結果造成很大的影響。所以,如果想要準備的測試代碼,需要等到應用穩定運行的時候。
前面提到了,unity的Console窗口開銷很大,所以我們在測試期間,不要使用log方法,但是如果我們確實需要詳細的數據打印, 就可以緩存log數據,然後再結束的時候打印出來,這樣就可以減少內存的消耗。在CustomTimer中我們使用了字符串拼接,這樣會導致大量的內存分配和GC。推薦使用stringbuilder。

最後分析思路

從某種角度來看性能優化就是一種減少消耗有效資源的不必要的任務。在使用各種數據收集工具時,可以總結爲三個不同的策略:
理解Profiler
瞭解它的一些特性,限制,優勢,可以從中得到更多的信息。比如,要意識到Timeline視圖的信息都是相對的,由於相對變換,大的問題也可能只有一個小的峯值。不要認爲大的峯值就是有問題。

減少干擾
數據源越多,就需要花費更多的時間去處理和過濾。所以其中一個最好的做法就是減少數據源,在timeline視圖,勾選checkbox就可以縮小數據的範圍。
將Gameobjects失活也可以阻止數據的採集,如果逐個的處理Gameobject時,性能突然變的更好了,就可以發現這個object是問題根源。

專注於問題
當沒有分析的時候,瓶頸重複和可見的存在,這就是個候選的問題。如果有的瓶頸在Profielr的時候一直存在,要記住這個瓶頸可能是我們的測試代碼引起的。

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