文章转自: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的结果进行缓存使用。不过很多人的理由是:
所以从Unity官方的手册来寻找关于GetCompnent的线索是最好的途径。的确,2011年的3.5.3版本的官方手册就已经建议减少使用GetCompnent方法来获取组件了,同时建议我们使用变量缓存获取的组件。
但是,我们可以发现手册上只说了频繁的调用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 5.x版本中,泛型版本的GetComponent<>的性能最好,而GetComponent(string)的性能最差。
做成柱状图可能更加直观:
接下来,我们来测试一下我们感兴趣的堆内存分配吧。为了更好的观察,我们把测试代码放在Update中执行。
[C#] 纯文本查看 复制代码
1
2
3
4
5
6
|
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会做更多的检测来保证正常运行。而这些堆内存的分配也是这种检测的结果,它会在找不到对应组件时在内部生成警告的字符串,从而造成了堆内存的分配。
所以各位不必担心使用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组件的效率:
[C#] 纯文本查看 复制代码
1
2
3
4
5
6
|
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<>,而且能够直接使用的属性访问器也更多,各位可以自己进行测试。