1. 程式人生 > 實用技巧 >Unity效能優化

Unity效能優化

常見的Unity效能問題

Unity效能問題

VSS:Virtual Set Size,虛擬耗用記憶體。它是一個程序能訪問的所有記憶體空間地址的大小。這個大小包含了 一些沒有駐留在RAM中的記憶體,就像mallocs已經被分配,但還沒有寫入。VSS很少用來測量程式的實際使 用記憶體。

RSS:Resident Set Size,實際使用實體記憶體。RSS是一個程序在RAM中實際持有的記憶體大小。RSS可能會 產生誤導,因為它包含了所有該程序使用的共享庫所佔用的記憶體,一個被載入到記憶體中的共享庫可能有很 多程序會使用它。RSS不是單個程序使用記憶體量的精確表示。

PSS:Proportional Set Size

,實際使用的實體記憶體,它與RSS不同,它會按比例分配共享庫所佔用的記憶體。 例如,如果有三個程序共享一個佔30頁記憶體控制元件的共享庫,每個程序在計算PSS的時候,只會計算10頁。 PSS是一個非常有用的數值,如果系統中所有的程序的PSS相加,所得和即為系統佔用記憶體的總和。當一個 程序被殺死後,它所佔用的共享庫記憶體將會被其他仍然使用該共享庫的程序所分擔。在這種方式下,PSS 也會帶來誤導,因為當一個程序被殺後,PSS並不代表系統回收的記憶體大小。

USS:Unique Set Size,程序獨自佔用的實體記憶體。這部分記憶體完全是該程序獨享的。USS是一個非常有用 的數值,因為它表明了執行一個特定程序所需的真正記憶體成本。當一個程序被殺死,USS就是所有系統回 收的記憶體。USS是用來檢查程序中是否有記憶體洩露的最好選擇。

DrawCall是CPU呼叫底層圖形介面的操作。比如有上千個物體,每一個的渲染都需要去呼叫一次底層介面,而每一次的呼叫CPU都需要做很多工作,那麼CPU必然不堪重負。

GC是用來處理記憶體回收的,但是卻增加了CPU的開銷(GC一次開銷可長可短,有時長達100ms)。因此對於GC的優化目標就是儘量少的觸發GC。

首先我們要知道所謂的GC是Mono執行時的機制,而非Unity3D遊戲引擎的機制,所以GC也主要是針對Mono的物件來說的,而它管理的也是Mono的託管堆。 明白了這一點,你也就明白了GC不是用來處理引擎的Assets(貼圖,音效,模型等等)的記憶體釋放的,因為U3D引擎也有自己的記憶體堆而不是和Mono一起使用所謂的託管堆。其次我們還要清楚什麼東西會被分配到託管堆上?對,就是引用型別。引用型別包括:使用者自定義的類,介面,委託,陣列,字串,Object.而值型別包括:幾種基本資料型別(如:int,float,bool等),結構體,列舉,空型別。所以GC的優化也就是程式碼的優化。

Unity執行時的記憶體佔用情況

Unity執行時的記憶體佔用情況

記憶體標準

  • 限定記憶體佔用不超過200M(iPhone4接近容易Crash,低端機型)

  • 專案中Reserved Total(總體分配)記憶體儘量控制在150M以內,如下 Texture 50M Mesh 20M AnimationClip 15M AudioClip 15M Mono堆記憶體 40M 字型等 10M

  • 專案中儘量嚴格控制,即使在中高階機型可較大記憶體執行。

Mono記憶體管理策略

  • 字串連線處理,建議StringBuilder

  • 儘量不使用foreach,Unity5.4以上解決了GC問題

  • 不要頻繁例項化和銷燬物件,建議物件池管理

  • 場景切換時,主動呼叫System.GC.Collect(),及時清理記憶體

Mono記憶體

  • Mono通過垃圾回收機制(Garbage Collect,簡稱GC)對記憶體進行管理。Mono記憶體分為兩部分,已用記憶體(used)和堆記憶體(heap),已用記憶體指的是mono實際需要使用的記憶體,堆記憶體指的是mono向作業系統申請的記憶體,兩者的差值就是mono的空閒記憶體。當mono需要分配記憶體時,會先檢視空閒記憶體是否足夠,如果足夠的話,直接在空閒記憶體中分配,否則mono會進行一次GC以釋放更多的空閒記憶體,如果GC之後仍然沒有足夠的空閒記憶體,則mono會向作業系統申請記憶體。

Mono記憶體洩漏分析

  • Mono通過引用關係,判斷哪些記憶體不再使用

  • 【Mono記憶體洩漏】物件已經不再使用,卻未被GC回收

  • Mono記憶體洩漏使空閒記憶體減少,GC頻繁,mono堆不斷擴大,最終導致遊戲記憶體佔用的增大

  • 大部分mono記憶體洩漏的情況都是由於靜態物件的引用引起

  • 不再需要的物件將其引用設定為null,使其可以被GC及時回收

解決方法

objA.arrInts設定為null,斷絕引用關係,待GC時,進行物件回收(objA本身是一個靜態物件,是GC的 根節點,因此沒有物件引用)

資源優化

  • Texture:解析度大小、格式、Mipmap、Read/Write
  1. Android 透明使用兩張ETC1壓縮(更高階一張ETC1上下Alpha分離),ETC2只支援OpenGL ES3.0裝置,在不支援的裝置上會自動轉成RGBA32/ARGB32格式,對於RGBA Compressed ETC2 8bits紋理記憶體佔用就增大4倍

  2. iOS 透明使用一張RGBA PVRTC 4bits或RGBA16或兩張RGB PVRTC Alpha分離,儘量不要使用RGBA32位

  3. 單張圖最大不超過1024*1024

  • Mesh:SubMesh數量、頂點數量、壓縮、Read/Write Mesh合併、不勾選Read/Write、大型場景使用LOD

  • AnimationClip:動畫曲線數量、Constant曲線數量、Dense曲線數量、Stream曲線數量、以及動畫事件數量

  • 動畫壓縮:無用的曲線刪除,調整float精度

  • AudioClp:格式、載入方式、時長以及頻率 BGM背景音:ogg SFX聲音特效:wav (有些專案BGM\SFX都用mp3)

  • Material:關聯的Shader和Texture

  • Shader儘量使用mobile速配的

ETC2 的格式理論上只在OpenGL ES 3.0 的裝置上被支援,而在不被支援的裝置上則會內部自動轉成 RGBA32/ARGB32的格式,這對於 RGBA Compressed ETC2 8bits 的紋理就是放大了 4 倍。因此,如果希望在 OpenGL ES 2.0 的裝置上對透明材質進行壓縮,那麼可以嘗試使用分離 Alpha 通道的方式,用兩個 ETC1 來進行壓縮

DrawCall優化

  • 先了解下DrawCall相關概念,便於優化

    DrawCall是CPU呼叫底層圖形介面的操作

    DrawCall_Num = 25K * CPU_Frame * CPU_Percentage / FPS

    DrawCall_Num : DrawCall數量(最大支援)

    CPU_Frame : CPU 工作頻率(GHz單位)

    CPU_Percentage:CPU 分配在DrawCall這件事情上的時間率 (百分比) ​ FPS:希望的遊戲幀率

  • DrawCall Batching(DC批處理)

    Dynamic Batching(動態批處理)

    Static Batching(靜態批處理)

  • Bus匯流排頻寬

    CPU完成一次DrawCall,除了需要呼叫一次DrawCall的命令之外,還需要把記憶體中頂點資料、紋理貼圖、shader引數通過bus匯流排拷貝到記憶體分配給GPU的視訊記憶體之中,注意這是拷貝,不是指標傳遞,速度不快。專案中不會同時出現的資源不要打包到一起,保證單張合併紋理不大於1024*1024一般就不會有問題了。

CPU優化最直接的方法

  • VSync(垂直同步)是CPU優化最直接的方式(發熱、耗電原因之一)

  • 開啟Edit-Project Settings-Quality找到V Sync Count

    V Sync Count

    Don’t Sync 不同步

    Every V Blank 每一個垂直同步

    Every Second V Blank 每一秒垂直同步

    通常我們選擇Don’t Sync,同時Application.targetFrameRate設定目標FPS,讓效能保持一個好的狀態。注意選擇其他項,Application.targetFrameRate設定不生效。

    科普:VSync垂直同步又稱場同步(Vertical Hold),垂直同步訊號決定了CRT從螢幕頂部畫到底部,再返回原始位置的時間。從CRT顯示器的顯示原理來看,單個畫素組成了水平掃描線,水平掃描線在垂直方向的堆積形成了完整的畫面。顯示器的重新整理率受顯示卡DAC控制,顯示卡DAC完成一幀的掃描後就會產生一個垂直同步訊號(決定於螢幕的重新整理率)。我們平時所說的開啟垂直同步指的是將該訊號送入顯示卡3D圖形處理部分,從而讓顯示卡在生成3D圖形時受垂直同步訊號的制約(注意是制約)。如果我們選擇等待垂直同步訊號(也就是我們平時所說的垂直同步開啟),那麼在遊戲中或許強勁的顯示卡迅速的繪製完一屏的影象,但是沒有垂直同步訊號的到達,顯示卡無法繪製下一屏,只有等垂直同步的訊號到達,才可以繪製。這樣FPS自然要受到作業系統重新整理率執行值的制約。而如果我們選擇不等待垂直同步訊號(也就是我們平時所說的關閉垂直同步),那麼遊戲中作完一屏畫面,顯示卡和顯示器無需等待垂直同步訊號就可以開始下一屏影象的繪製,自然可以完全發揮顯示卡的實力。但是不要忘記,正是因為垂直同步的存在,才能使得遊戲程序和顯示器重新整理率同步,使得畫面更加平滑和穩定。取消了垂直同步訊號,固然可以換來更快的幀率,但是在影象的連續性上勢必打折扣。

GPU優化

渲染流程

GPU接收頂點資料作為輸入傳遞給頂點著色器。頂點著色器的處理單元是頂點,輸入進來的每個頂點都會呼叫一次頂點著色器。(頂點著色器本身不可以建立或銷燬任何頂點,並無法得到頂點與頂點之間的關係)。頂點著色器是完全可程式設計的,它主要完成的工作有:座標變換和逐頂點光照。 座標變換:就是對頂點的座標進行某種變換—把頂點座標從模型空間轉換到齊次裁剪空間。頂點的多少直接決定了三角形面的多少,也直接決定了GPU的渲染流水線的工作量,所以減少頂點數是一個比較重要的優化點。那麼減少頂點怎麼操作呢,又有哪些途徑?

  • 頂點著色器 優化基本幾何體(模型減面減頂點) 使用LOD(Level of detail)技術 使用遮擋剔除(Occlusion culling)技術

  • 中間操作 曲面細分著色器:是一個可選的著色器,主要用於細分圖元 幾何著色器:是一個可選的著色器,可用於執行逐圖元的著色操作,或者被用於產生更多的圖元。 裁剪:這一階段是可配置的。目的是把那些不在視野內的頂點裁剪掉,並剔除某些三角形圖元的面片。部分在視野內的圖元需要做裁剪處理,在裁剪邊緣產生新的頂點和三角形進行處理。 螢幕對映:這一階段是可配置和程式設計的,負責把每個圖元的座標(三維座標系)轉換成螢幕座標(二維座標系)。

  • 三角形設定:開始進入光柵化階段,不再是數學上點了,而會把所有的點都對映到螢幕的具體畫素座標上,計算每條邊上的畫素座標而得到三角形邊界的表示方式即為三角形設定。 三角形遍歷:這一階段會檢查每個畫素是否被一個三角風格所覆蓋。如果覆蓋的話,就會生成一個片元(一個片元並不是真正意義上的畫素,而是包含了很多狀態的集合,這些狀態用於計算每個畫素的最終顏色。這些狀態包括了螢幕座標、深度資訊,及從幾何階段輸出的頂點資訊,如法線和紋理座標等。),這樣一個查詢哪些畫素被三角形覆蓋的過程就是三角形遍歷。

  • 片元著色器 儘量減少overdraw 減少實時光照 不要使用動態陰影 儘量使用簡單的shader

片元著色器的輸入就是上一階段對頂點資訊插值得到的結果,更具體點說,是根據從頂點著色器中輸出的資料插值得到的。而這一階段的輸出是一個或者多個顏色值。這一階段可以完成很多重要的渲染技術,如紋理取樣,但是它的侷限在於,它僅可以影響單個片元。片元著色器是比較花時間的,因為它是最終顏色的計算者,在某些情況下,例如複雜燈光環境下,片元著色器會出現GPU流水線主要的拖後腿的存在。為了讓片元著色器的計算更加快,我們需要從很多方面進行提前的優化:片元著色器最容易拖後腿的情況就是,overdraw!和Android app的開發一樣,就是同一個畫素點繪製了多次,某些情況會造成計算力的浪費,增加耗電量。前面提到的遮擋剔除有減少overdraw非常有用。在PC上,資源無限,為了得到最準確的渲染結果,繪製順序可能是從後往前繪製不透明物體,然後再繪製透明物體進行混合。但是在移動平臺上,對於不透明物體,我們可以設定從前往後繪製,對於有透明通道的物體(很多UI紋理就是含有透明通道的),再設定從後往前繪製。unity中shader設定為“Geometry” 佇列的物件總是從前往後繪製的,而其他固定佇列(如“Transparent”“Overla”等)的物體,則都是從後往前繪製的。這意味這,我們可以儘量把物體的佇列設定為“Geometry” 。對於GUI,尤其要注意和設計師商量,能用不透明的設計就用不透明的,對於粒子效果,也要注意不要引入透明值,多半情況下,移動平臺的粒子效果透明值沒有作用。

移動平臺的最大敵人。一個場景裡如果包含了三個逐畫素的點光源,而且使用了逐畫素的shader,那麼很有可能將Draw Calls提高了三倍,同時也會增加overdraws。這是因為,對於逐畫素的光源來說,被這些光源照亮的物體要被再渲染一次。更糟糕的是,無論是動態批處理還是動態批處理(其實文件中只提到了對動態批處理的影響,但不知道為什麼實驗結果對靜態批處理也沒有用),對於這種逐畫素的pass都無法進行批處理,也就是說,它們會中斷批處理。所以當你需要光照效果時,可以使用Lightmaps,提前烘焙好,提前把場景中的光照資訊儲存在一張光照紋理中,然後在執行時刻只需要根據紋理取樣得到光照資訊即可。當你需要金屬性強(鏡面)的效果,可以使用Light Probes。當你需要一束光的時候,可以使用體積光去模擬這個效果。

動態陰影很酷,但是對於片元著色器來說是災難,陰影計算是三角投影計算,非常耗效能。如果想要陰影,可以使用

  1. 簡單的使用一個帶陰影的貼圖
  1. 烘焙場景,拿到lightmaps
  1. 建立投影生成器的方法
  1. 使用ShadowMap的方法
  1. 建議儘量使用Unity自帶mobile版本的(built-in)Shader,這些大大提高了頂點處理的效能。當然也會有一些限制。

  2. 自己寫的shader請注意複雜操作符計算,類似pow,exp,log,cos,sin,tan等都是很耗時的計算,最多隻用一次在每個畫素點的計算,還有有些除法運算儘量該能乘法運算等。

  3. 避免透明度測試著色器,因為這個非常耗時,使用透明度混合的版本來代替。

  4. 浮點型別運算:精度越低的浮點計算越快。

  5. 不要在Shader中新增不必要的Pass.

Unity優化工具

  • MAT(Memory Analyzer Tool) 需要匯入HPROF檔案再分析 只能檢視java層的記憶體情況,看不到native堆的詳情

  • Xcode Instrument工具 只能用於Mac,iOS 只能檢視C++ 或 object C 的情況,看不到mono堆的詳情

  • Unity自帶Profiler 需要單獨編譯develop版本 在PC上執行,沒法捕獲真機資料 記憶體資料跟實際真機的資料差異很大、多的時候有幾十M差距 只能看到最近一段時間的資料,看不到總體的詳情

  • 官方開源Memory Profiler

  1. Unity5.3及其以上

  2. 使用IL2CPP,比如iOS平臺

  3. 構建時開啟Development Build

研發團隊需要關注的引擎模組

優化關注模組

開始優化工作

前面已經介紹了效能相關概念以及需關注模組,接下來該開始優化工作,具體步驟如下:

優化工作