再议Unity的优化

0x00 前言

在很长一段时间里,Unity项目的开发者的优化指南上基本都会有一条关于使用GetCompnent方法获取组件的条目(例如14年我的这篇博客《深入浅出聊Unity3D项目优化:从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<T>()
  • GetComponent(typeof(T))
  • GetComponent(string)

所以,测试的第一步就是先确定一个效率最高的重载版本,之后再去检查它们各自引起的堆内存分配。

“效率之王”

为此,我们在5.X版本的Unity中准备一个空白的场景并实现一个简单的计时器,之后就可以开始测试了。

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次以便于观察测试结果:

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


QQ截图20170506163532.png

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

做成柱状图可能更加直观:

QQ截图20170506163819.png

接下来,我们来测试一下我们感兴趣的堆内存分配吧。为了更好的观察,我们把测试代码放在Update中执行。

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

同样每帧执行1000000次的GetComponent<T>方法。打开profiler来观察一下堆内存分配吧:

QQ截图20170506204741.png

我们可以发现,虽然频繁调用GetComponent<T>时会造成CPU的开销很大,但是堆内存分配却是0B

但是,我和朋友聊天偶尔聊到这个话题时,朋友说有时候会发现每次调用GetComponent<T>时,在profiler中都会增加0.5kb的堆内存分配。不知道各位读者是否有遇到过这个问题,那么是不是说GetComponent方法有时的确会造成GC呢?

答案是否定的。

这是因为朋友是在Editor中运行,并且GetComponent<T>返回Null的情况下,才会出现堆内存分配的问题。
我们还可以继续我们的测试,这次把TestComp组件从场景中去除,同时把测试次数改为100000。我们在Editor运行测试,可以看到结果如下:

QQ图片20170506210207.png

10000次调用GetComponent方法,并且返回为Null时,观察Editor的Profiler,可以发现每一帧都分配了5.6MB的堆内存。

那么如果在移动平台上调用GetComponent方法,并且返回为Null时,是否会造成堆内存分配呢?

这次我们让这个测试跑在一个小米4的手机上,连接profiler观察堆内存分配,结果如图:

QQ图片20170506212045.png

可以发现,在手机上并不会产生堆内存的分配。

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<>来获取它们:

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,性能要远好于调用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<>,而且能够直接使用的属性访问器也更多,各位可以自己进行测试。

-分割线-
最后打个广告,欢迎支持我的书《Unity 3D脚本编程》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,142评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,298评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,068评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,081评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,099评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,071评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,990评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,832评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,274评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,488评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,649评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,378评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,979评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,625评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,643评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,545评论 2 352

推荐阅读更多精彩内容