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



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

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

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

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

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



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

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

答案是否定的。

这是因为朋友是在Editor中运行,并且GetComponent返回Null的情况下,才会出现堆内存分配的问题。

我们还可以继续我们的测试,这次把TestComp组件从场景中去除,同时把测试次数改为100000。我们在Editor运行测试,可以看到结果如下:



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

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

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

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

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

所以从4.x升级到5.x之后,这些访问器就无法使用了,所以升级引擎时各位可以关注一下自己的代码中是否有类似的问题。

好了,我们接着在测试中加入使用访问器获取Transform组件的效率:

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

运行1000000次,结果如下(单位ms)

单次的耗时是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脚本编程》~

再议Unity优化的更多相关文章

  1. 再议Unity 3D

    一年前,偶发冲动,翻译了<[译] Unity3D游戏和facebook绑定(1:简介)>系列文章. 现在看有2个明显的好处, 一:给这个不温不火的博客带了top 3的人气: 二:我个人由此 ...

  2. Unity教程之再谈Unity中的优化技术

    这是从 Unity教程之再谈Unity中的优化技术 这篇文章里提取出来的一部分,这篇文章让我学到了挺多可能我应该知道却还没知道的知识,写的挺好的 优化几何体   这一步主要是为了针对性能瓶颈中的”顶点 ...

  3. [Unity优化] Unity CPU性能优化

    前段时间本人转战unity手游,由于作者(Chwen)之前参与端游开发,有些端游的经验可以直接移植到手游,比如项目框架架构.代码设计.部分性能分析,而对于移动终端而言,CPU.内存.显卡甚至电池等硬件 ...

  4. 再议 js 数字格式之正则表达式

    原文:再议 js 数字格式之正则表达式 前面我们提到到了js的数字格式<浅谈 js 数字格式类型>,之前的<js 正则练习之语法高亮>里也提到了优化数字匹配的正则.不过最近落叶 ...

  5. 再议Java中的static关键字

    再议Java中的static关键字 java中的static关键字在很久之前的一篇博文中已经讲到过了,感兴趣的朋友可以参考:<Java中的static关键字解析>. 今天我们再来谈一谈st ...

  6. (转)Unity优化之减少Drawcall

    转载:http://www.jianshu.com/p/061e67308e5f Unity GUI(uGUI)使用心得与性能总结 背景和目的 小哈接触Unity3D也有一段时间了,项目组在UI解决方 ...

  7. 【Unity优化】构建一个拒绝GC的List

    版权声明:本文为博主原创文章,欢迎转载.请保留博主链接:http://blog.csdn.net/andrewfan 上篇文章<[Unity优化]Unity中究竟能不能使用foreach?> ...

  8. Python学习之再议row_input

    再议raw_input birth = raw_input('birth: ') if birth < 2000: print '00前' else: print '00后' 运行结果: bir ...

  9. UNITY 优化之带Animator的Go.SetActive耗时问题,在手机上,这个问题似乎并不存在,因为优化了后手机上运行帧率并未明显提升

    UNITY 优化之带Animator的Go.SetActive耗时问题,在手机上,这个问题似乎并不存在,因为优化了后手机上运行帧率并未明显提升 经确认,这个问题在手机上依然存在,不过占的比例非常小.因 ...

随机推荐

  1. 【翻译】CSS水平和垂直居中的12种方法

    英语原文链接 在CSS中有许多不同的方法能够做到水平和垂直居中,但很难去选择合适的那个.我会向你展示我所看到的所有的方法,帮助你在所面对的情境下选择最棒的那一个. 方法1 此方法将只能垂直居中单行文本 ...

  2. 用Stax方式处理xml

    1.读取xml文件,首先用类加载器加载项目目录下的xml文件,从XMLInputFactory创建我所需要的XMLStreamReader,即得到了xml文件.根据XMLStreamConstant ...

  3. iOS开发之UIDevice通知

    UIDevice类提供了一个单例对象,它代表着设备,通过它可以获得一些设备相关的信息,比如电池电量值(batteryLevel).电池状态(batteryState).设备的类型(model,比如iP ...

  4. Linux之cut命令

    cut 参数: -d  指定分隔符,与-f 一起使用,默认是空格.例如:-d“|” -f  指定取第几段的数据与-d一起使用 -c  以字符为单位取出固定字符区间 示例: 取不连续区间的内容的时候使用 ...

  5. Maven 自定义 archetype

    最近在公司经常要写一些 storm-job 工程的骨架(archetype)非常相似,为了能够将大家的工程结构固定下来以及节约建工程的成本,所以给组内自定义了maven-archetype,中途遇到了 ...

  6. Ubuntu14.04配置Apache支持多个站点

    怎样在一个Ubuntu的机器上(虚拟机)配置Apache支持多个网站呢? 比如你有一台独立的Ubuntu虚拟机,配有一个外网的IP(45.46.47.48),并且注册了两个域名AAA.com和BBB. ...

  7. 异步编程的两种模型,闭包回调,和Lua的coroutine,到底哪一种消耗更大

    今天和人讨论了一下CPS变形为闭包回调(典型为C#和JS),以及Lua这种具有真正堆栈,可以yield和resume的coroutine,两种以同步的形式写异步处理逻辑的解决方案的优缺点.之后生出疑问 ...

  8. DataTable源码分析(二)

    DataTable源码分析(二) ===================== DataTable函数分析 ---------------- DataTable作为整个插件的入口,完成了整个表格的数据初 ...

  9. C++中的类继承(2)派生类的默认成员函数

    在继承关系里面, 在派生类中如果没有显示定义这六个成员 函数, 编译系统则会默认合成这六个默认的成员函数. 构造函数. 调用关系先看一段代码: class Base { public : Base() ...

  10. Sitemesh 3 配置和使用(最新)

    Sitemesh 3 配置和使用(最新) 一 Sitemesh简介 Sitemesh是一个页面装饰器,可以快速的创建有统一外观Web应用 -- 导航 加 布局 的统一方案~ Sitemesh可以拦截任 ...