再議Unity的優(yōu)化

0x00 前言

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

0x01 來自官方的建議

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

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

所以從Unity官方的手冊來尋找關于GetCompnent的線索是最好的途徑。的確,2011年的3.5.3版本的官方手冊就已經(jīng)建議減少使用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).

但是,我們可以發(fā)現(xiàn)手冊上只說了頻繁的調(diào)用GetComponent會導致CPU的開銷增加,但是并沒有提到GC的問題。所以,為了驗證GetComponent到底會導致哪些性能上的問題,我們可以做幾個小測試。

0x02 和GC無關的性能優(yōu)化

眾所周知,GetComponent有三個重載版本,分別是:

  • GetComponent<T>()
  • GetComponent(typeof(T))
  • GetComponent(string)

所以,測試的第一步就是先確定一個效率最高的重載版本,之后再去檢查它們各自引起的堆內(nèi)存分配。

“效率之王”

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

using System;
using System.Diagnostics;

/// <summary>
/// 簡易的計時類
/// </summary>
public class YiWatch : IDisposable
{
    #region 字段

    private string testName;
    private int testCount;
    private Stopwatch watch;

    #endregion


    #region 構造函數(shù)

    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}    測試數(shù)量:{3}",
            this.testName, totalTime, totalTime / this.testCount, this.testCount));
    }

    #endregion

}

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

    int testCount = 1000000;//定義測試的次數(shù)

    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):


QQ截圖20170506163532.png

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

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

QQ截圖20170506163819.png

接下來,我們來測試一下我們感興趣的堆內(nèi)存分配吧。為了更好的觀察,我們把測試代碼放在Update中執(zhí)行。

void Update()
{
    for(int i = 0; i < testCount; i++)
    {
        GetComponent<TestComp>();
    }
}

同樣每幀執(zhí)行1000000次的GetComponent<T>方法。打開profiler來觀察一下堆內(nèi)存分配吧:

QQ截圖20170506204741.png

我們可以發(fā)現(xiàn),雖然頻繁調(diào)用GetComponent<T>時會造成CPU的開銷很大,但是堆內(nèi)存分配卻是0B

但是,我和朋友聊天偶爾聊到這個話題時,朋友說有時候會發(fā)現(xiàn)每次調(diào)用GetComponent<T>時,在profiler中都會增加0.5kb的堆內(nèi)存分配。不知道各位讀者是否有遇到過這個問題,那么是不是說GetComponent方法有時的確會造成GC呢?

答案是否定的。

這是因為朋友是在Editor中運行,并且GetComponent<T>返回Null的情況下,才會出現(xiàn)堆內(nèi)存分配的問題。
我們還可以繼續(xù)我們的測試,這次把TestComp組件從場景中去除,同時把測試次數(shù)改為100000。我們在Editor運行測試,可以看到結果如下:

QQ圖片20170506210207.png

10000次調(diào)用GetComponent方法,并且返回為Null時,觀察Editor的Profiler,可以發(fā)現(xiàn)每一幀都分配了5.6MB的堆內(nèi)存。

那么如果在移動平臺上調(diào)用GetComponent方法,并且返回為Null時,是否會造成堆內(nèi)存分配呢?

這次我們讓這個測試跑在一個小米4的手機上,連接profiler觀察堆內(nèi)存分配,結果如圖:

QQ圖片20170506212045.png

可以發(fā)現(xiàn),在手機上并不會產(chǎn)生堆內(nèi)存的分配。

Null Check造成的困惑

那么這是為什么呢?其實這種情況只會發(fā)生在運行在Editor的情況下,因為Editor會做更多的檢測來保證正常運行。而這些堆內(nèi)存的分配也是這種檢測的結果,它會在找不到對應組件時在內(nèi)部生成警告的字符串,從而造成了堆內(nèi)存的分配。

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會造成額外的堆內(nèi)存分配了。同時也可以發(fā)現(xiàn)只要不頻繁的調(diào)用GetComponent方法,CPU的開銷還是可以接受的。但是頻繁的調(diào)用GetComponent會造成顯著的CPU的開銷的情況下,各位還是對組件進行緩存的好。

屬性訪問器的性能

既然聊了GetComponent方法的性能,接下來我們可以繼續(xù)來聊聊和GetComponent功能有些類似的,Unity腳本系統(tǒng)中的一些屬性訪問器的性能。
我們最常見的屬性訪問器大概算是transform和gameObject了吧,當然,如果使用過4.x版本的朋友應該還會知道rigidbody、camera、renderer等等。但是到了5.x時代,除了gameObject和transform之外的屬性訪問器都已經(jīng)被棄用了,相反,5.x中會使用 GetComponent<>來獲取它們:

QQ截圖20170507151205.png

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

好了,我們接著在測試中加入使用訪問器獲取Transform組件的效率:

    using (new YiWatch("transform", testCount))
    {
        for(int i = 0; i < testCount; i++)
        {
            transformTest = this.transform;
        }
    }

運行1000000次,結果如下(單位ms)

QQ截圖20170507152432.png

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

0x03 總結

通過以上測試,我們可以發(fā)現(xiàn):

  • 頻繁的調(diào)用GeComponent方法會造成CPU的開銷,但是對GC幾乎沒有影響。
  • Profiler不要用來分析Editor中運行的項目,由于一些引擎內(nèi)部的檢查會導致結果出現(xiàn)較大偏差。
  • 5.X版本中GeComponent<>的性能最好。
  • 使用屬性訪問器來訪問一些內(nèi)建的屬性例如transform的性能已經(jīng)可以讓人接受了,并不一定非要緩存這些屬性。
  • 5.X版本刪掉了很多屬性訪問器,基本上只保留了gameObject和transform。

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

-分割線-
最后打個廣告,歡迎支持我的書《Unity 3D腳本編程》

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容