天天看點

Unity優化 詳談GeComponent

文章轉自:http://www.manew.com/thread-104010-1-1.html?_dsign=aaa7cc41

0x00 前言

在很長一段時間裡,Unity項目的開發者的優化指南上基本都會有一條關于使用GetCompnent方法擷取元件的條目(例如14年我的這篇部落格《深入淺出聊項目優化:從Draw Calls到GC》)。有時候還會發展為連一些Unity内部對象的屬性通路器都要小心使用的注意事項,記得曾經有一段時間我們的項目組也會嚴格要求把例如transform、gameobject之類的屬性通路器進行緩存使用。這其中的确有一些措施是有道理的,但很多朋友卻也是知其然而不知其是以然,朦胧之間似乎有一個印象,進而成為習慣。那麼本文就來聊聊Unity優化這個題目中偶爾會被誤解的内容吧。

0x01 來自官方的建議

本文主要是關于Unity腳本優化的,而腳本和引擎打交道的一個常見情景便是使用GetComponent之類的方法, 接觸過Unity的朋友大都知道要将GetComponent的結果進行緩存使用。不過很多人的理由是:

使用GetComponent會造成GC,進而影響效率。

是以從Unity官方的手冊來尋找關于GetCompnent的線索是最好的途徑。的确,2011年的3.5.3版本的官方手冊就已經建議減少使用GetCompnent方法來擷取元件了,同時建議我們使用變量緩存擷取的元件。

Reduce GetComponent Calls

Using GetComponent or built-in component accessors can have a noticeable overhead. You can avoid this by getting a reference to the component once and assigning it to a variable (sometimes referred to as "caching" the reference).

但是,我們可以發現手冊上隻說了頻繁的調用GetComponent會導緻CPU的開銷增加,但是并沒有提到GC的 問題 。是以,為了驗證GetComponent到底會導緻哪些性能上的問題,我們可以做幾個小測試。

0x02 和GC無關的性能優化

衆所周知,GetComponent有三個重載版本,分别是:

GetComponent()

GetComponent(typeof(T))

GetComponent(string)

是以,測試的第一步就是先确定一個效率最高的重載版本,之後再去檢查它們各自引起的堆記憶體配置設定。

“效率之王”

為此,我們在5.X版本的Unity中準備一個空白的場景并實作一個簡單的計時器,之後就可以開始測試了。

[C#]  純文字檢視  複制代碼 ?

  01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44

using

System;

using

System.Diagnostics;

/// <summary>

/// 簡易的計時類

/// </summary>

public

class

YiWatch : IDisposable

{

#region 字段

private

string

testName;

private

int

testCount;

private

Stopwatch watch;

#endregion

#region 構造函數

public

YiWatch(

string

name,

int

count)

{

this

.testName = name;

this

.testCount = count > 0 ? count : 1;

this

.watch = Stopwatch.StartNew();

}

#endregion

#region 方法

public

void

Dispose()

{

this

.watch.Stop();

float

totalTime =

this

.watch.ElapsedMilliseconds;

UnityEngine.Debug.Log(

string

.Format(

"測試名稱:{0}   總耗時:{1}   單次耗時:{2}    測試數量:{3}"

,

this

.testName, totalTime, totalTime /

this

.testCount,

this

.testCount));

}

#endregion

}

自定義的元件TestComp,以及我們的測試代碼,每一個方法會被調用1000000次以便于觀察測試結果:

[C#]  純文字檢視  複制代碼 ?

  01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

int

testCount = 1000000;

//定義測試的次數

using

(

new

YiWatch(

"GetComponent<>"

, testCount))

{

for

(

int

i = 0; i < testCount; i++)

{

GetComponent<TestComp>();

}

}

using

(

new

YiWatch(

"GetComponent(typeof(T))"

, testCount))

{

for

(

int

i = 0; i < testCount; i++)

{

GetComponent(

typeof

(TestComp));

}

}

using

(

new

YiWatch(

"GetComponent(string)"

, testCount))

{

for

(

int

i = 0; i < testCount; i++)

{

GetComponent(

"TestComp"

);

}

}

運作的結果如圖(機關ms):

Unity優化 詳談GeComponent

我們可以發現在Unity 5.x版本中,泛型版本的GetComponent<>的性能最好,而GetComponent(string)的性能最差。

做成柱狀圖可能更加直覺:

Unity優化 詳談GeComponent

接下來,我們來測試一下我們感興趣的堆記憶體配置設定吧。為了更好的觀察,我們把測試代碼放在Update中執行。

[C#]  純文字檢視  複制代碼 ?

  1 2 3 4 5 6

void

Update()

{

for

(

int

i = 0; i < testCount; i++)

{

GetComponent<TestComp>();

}

}

同樣每幀執行1000000次的GetComponent方法。打開profiler來觀察一下堆記憶體配置設定吧:

Unity優化 詳談GeComponent

我們可以發現,雖然 頻繁調用GetComponent時會造成CPU的開銷很大,但是堆記憶體配置設定卻是0B。

但是,我和朋友聊天偶爾聊到這個 話題 時,朋友說有時候會發現每次調用GetComponent時,在profiler中都會增加0.5kb的堆記憶體配置設定。不知道各位讀者是否有遇到過這個問題,那麼是不是說GetComponent方法有時的确會造成GC呢?

答案是否定的。

這是因為朋友是 在Editor中運作 ,并且 GetComponent傳回Null 的情況下,才會出現堆記憶體配置設定的問題。

我們還可以繼續我們的測試,這次把TestComp元件從場景中去除,同時把測試次數改為100000。我們在Editor運作測試,可以看到結果如下:

Unity優化 詳談GeComponent

10000次調用GetComponent方法,并且傳回為Null時,觀察Editor的Profiler,可以發現每一幀都配置設定了5.6MB的堆記憶體。

那麼如果在移動平台上調用GetComponent方法,并且傳回為Null時,是否會造成堆記憶體配置設定呢?

這次我們讓這個測試跑在一個小米4的手機上,連接配接profiler觀察堆記憶體配置設定,結果如圖:

Unity優化 詳談GeComponent

可以發現,在手機上并不會産生堆記憶體的配置設定。

Null Check造成的困惑

那麼這是為什麼呢?其實這種情況隻會發生在運作在Editor的情況下,因為Editor會做更多的檢測來保證正常運作。而這些堆記憶體的配置設定也是這種檢測的結果,它會在找不到對應元件時在内部生成警告的字元串,進而造成了堆記憶體的配置設定。

We do this in the editor only. This is why when you call GetComponent() to query for a component that doesn’t exist, that you see a C# memory allocation happening, because we are generating this custom warning string inside the newly allocated fake null object.

是以各位不必擔心使用GetComponent會造成額外的堆記憶體配置設定了。同時也可以發現隻要不頻繁的調用GetComponent方法,CPU的開銷還是可以接受的。但是頻繁的調用GetComponent會造成顯著的CPU的開銷的情況下,各位還是對元件進行緩存的好。

屬性通路器的性能

既然聊了GetComponent方法的性能,接下來我們可以繼續來聊聊和GetComponent功能有些類似的,Unity腳本系統中的一些屬性通路器的性能。

我們最常見的屬性通路器大概算是transform和gameObject了吧,當然,如果使用過4.x版本的朋友應該還會知道rigidbody、camera、renderer等等。但是到了5.x時代,除了gameObject和transform之外的屬性通路器都已經被棄用了,相反,5.x中會使用 GetComponent<>來擷取它們:

Unity優化 詳談GeComponent

是以從4.x更新到5.x之後,這些通路器就無法使用了,是以更新引擎時各位可以關注一下自己的代碼中是否有類似的問題。

好了,我們接着在測試中加入使用通路器擷取Transform元件的效率:

[C#]  純文字檢視  複制代碼 ?

  1 2 3 4 5 6

using

(

new

YiWatch(

"transform"

, testCount))

{

for

(

int

i = 0; i < testCount; i++)

{

transformTest =

this

.transform;

}

}

運作1000000次,結果如下(機關ms)

Unity優化 詳談GeComponent

單次的耗時是0.000026ms,性能要遠好于調用GetComponent<>方法,是以是否緩存類似gameObject或者transform這樣的屬性通路器似乎對性能的優化幫助不大。當然寫代碼和個人的習慣關系很大,如果各位早已習慣緩存這些屬性通路器自然也是不錯的選擇。

0x03 總結

通過以上測試,我們可以發現:

頻繁的調用GeComponent方法會造成CPU的開銷,但是對GC幾乎沒有影響。

Profiler不要用來分析Editor中運作的項目,由于一些引擎内部的檢查會導緻結果出現較大偏差。

5.X版本中GeComponent<>的性能最好。

使用屬性通路器來通路一些内建的屬性例如transform的性能已經可以讓人接受了,并不一定非要緩存這些屬性。

5.X版本删掉了很多屬性通路器,基本上隻保留了gameObject和transform。

最後需要說明的是,上述的測試發生在5.X版本的Unity中。如果使用4.x版本可能會有些許不同,例如在4.X版本中,GetComponent(typeof)的性能可能要好于GetComponent<>,而且能夠直接使用的屬性通路器也更多,各位可以自己進行測試。

繼續閱讀