目录
Find 和 FindObjectOfType
Camera.main
按 ID 寻址
与 UnityEngine.Object 子类进行 Null 比较
矢量和四元数数学以及运算顺序
使用非分配物理 API
Update 管理器
减少方法调用开销
简单属性
简单方法
一般来说,最好完全避免在生产代码中使用
Object.Find
和Object.FindObjectOfType
。
由于此类 API 要求 Unity 遍历内存中的所有游戏对象和组件,因此它们会随着项目规模的扩大而产生性能问题。
在单例对象的访问器对上述规则来说是个例外。全局管理器对象往往会暴露“instance”属性,并且通常在 getter 中存在 FindObjectOfType
调用以便检测单例预先存在的实例。虽然这种模式通常是可以接受的,但必须注意检查代码并确保调用访问器时场景中不存在单例对象。如果 getter 没有自动创建缺失单例的实例,那么寻找单例的代码经常会重复调用 FindObjectOfType
(通常每帧多次发生)并且会对性能产生不良影响。
Camera.main
如果代码必须寻址 Main Camera,强烈建议使用下面的任意一种方式:
在
Start()
或OnEnable()中访问
Camera.main
,并缓存返回的这个引用。构造一个
Camera Manager
类,可提供或添加对活动摄像机对引用。
在内部,Unity 的 Camera.main
属性会调用 Object.FindObjectWithTag
(这是 Object.FindObject
的一个专用变体)。访问此属性并不比调用 Object.FindObjectOfType
更高效。
每当在 Animator、Material 或 Shader 上使用 Set 或 Get 方法时,请使用整数值方法而非字符串值方法。
Unity 不使用字符串名称对 Animator、Material 和 Shader 属性进行内部寻址。为了加快速度,所有属性名称都经过哈希处理为属性 ID,实际上正是这些 ID 用于寻址属性。字符串方法只执行字符串哈希处理,然后将经过哈希处理的 ID 转发给整数值方法。
从字符串哈希创建的属性 ID 在单次运行过程中是不变的。它们最简单的用法是为每个属性名称声明一个静态只读整数变量,然后使用整数变量代替字符串。启动期间将自动进行初始化,无需其他初始化代码。
Animator.StringToHash 是用于 Animator 属性名称的对应 API,Shader.PropertyToID 是用于 Material 和 Shader 属性名称的对应 API。
对于从 UnityEngine.Object 派生的类的实例,与 Null 进行比较的成本虽然低,但远高于与纯 C# 类进行比较的成本。因此,请避免在紧凑循环中或每帧运行的代码中进行此类 Null 比较。
Mono 和 IL2CPP 运行时以特定方式处理从 UnityEngine.Object 派生的类的实例。在实例上调用方法实际上是调用引擎代码,此过程必须执行查找和验证以便将脚本引用转换为对原生代码的引用。
运算由快到慢:int > float > Vector
对于位于紧凑循环中的矢量和四元数运算,请记住整数(int)运算比浮点数(float)更快,而浮点数(float)比矢量、矩阵或四元数(Vector2/3/4)运算更快。
因此,每当交换或关联算术允许时,请尝试最小化单个数学运算的成本:
Vector3 vector;
int a;
int b;
Vector3 slow = a * vector * b; // 效率较低:产生两次矢量乘法
Vector3 fast = a * b * vector; // 效率较高:一次整数乘法、一次矢量乘法
将 RaycastAll 调用替换为 RaycastNonAlloc,将 SphereCastAll 调用替换为 SphereCastNonAlloc,以此类推。
在 Unity 5.3 及更高版本中,引入了所有物理查询 API 的非分配版本。对于 2D 应用程序,也存在所有 Physics2D 查询 API 的非分配版本。
在内部,Unity 会跟踪感兴趣的列表中的对象的回调(例如 Update
、FixedUpdate
和 LateUpdate
)。这些列表以侵入式链接列表的形式进行维护,从而确保在固定时间进行列表更新。在启用或禁用 MonoBehaviour 时分别会在这些列表中添加/删除 MonoBehaviour。
随着回调数量的增加,这种方式将变得越来越低效。从原生代码调用托管代码回调有一个很小但很明显的开销。这会导致在调用大量每帧都执行的方法时延长帧时间,而且在实例化包含大量 MonoBehaviour 的预制件时延长实例化时间(注意: 实例化成本归因于调用预制件中每个组件上的 Awake 和 OnEnable 回调时产生的性能开销)。
当具有每帧回调的 MonoBehaviour 数量增长到数百或数千时,移除这些回调并将 MonoBehaviour(甚至标准 C# 对象)附加到一个global manager singleton单例可以优化性能。然后,global manager单例可将 Update
、LateUpdate
和其他回调分发给感兴趣的对象。这还有一个额外的好处,就是允许代码在没有操作的情况下巧妙地取消订阅回调,从而减少了大量每帧必须调用的函数的数量。
性能上最大的节约来自于消除很少执行的回调。请考虑以下伪代码:
void Update() {
if(!someVeryRareCondition) { return; }
// … 某些操作 …
}
如果大量 MonoBehaviour 具有上述类似 Update 回调,则运行 Update 回调所使用的大量时间会用于原生和托管代码域之间的切换以便执行 MonoBehaviour之后再立即退出。如果这些类仅在 someVeryRareCondition
== true 时订阅global Update Manager ,此后便退订,则可节省代码域切换和稀有条件评估所需的时间。
调用的每个方法都必须在内存中找到该方法的地址,并将另一个帧推入栈。所有这些操作都是有成本的,但在大多数代码中,它们都小到可以忽略不计。
但是,在紧凑循环中运行较小的方法时,因引入额外方法调用而增加的开销可能会变得非常显著,甚至占主导地位。请考虑以下两个简单方法。
Example 1:
int Accum { get; set; }
Accum = 0;
for(int i = 0; i < myList.Count; i++) {
Accum += myList[i];
}
Example 2:
int accum = 0;
int len = myList.Count;
for(int i = 0; i < len; i++) {
accum += myList[i];
}
两种方法都计算List中所有int之和。第一个示例是更“现代的 C#”,它使用自动生成的属性来保存其数据值。
虽然从表面上看这两段代码似乎是等效的,但通过分析代码中的方法调用情况,可看出差异很明显。
示例 1:
int Accum { get; set; }
Accum = 0;
for(int i = 0;
i < myList.Count; // 调用 List::getCount
i++) {
Accum // 调用 set_Accum
+= // 调用 get_Accum
myList[i]; // 调用 List::get_Value
}
每次循环执行时都有四个方法调用:
myList.Count
调用 Count
属性上的 get
方法Accum
属性上的 get
和 set
方法肯定要调用get
检索 Accum
的当前值,以便将其传递给加法运算set
将加法运算的结果分配给 Accum
示例 2:
int accum = 0;
int len = myList.Count;
for(int i = 0;
i < len;
i++) {
accum += myList[i]; // 调用 List::get_Value
}
在第二个示例中,get_Value
调用仍然存在,但已删除所有其他方法或不再是每个循环迭代便执行一次。
由于 accum
现在是原始值而不是属性,因此不需要进行方法调用来set或get其值。
由于假设 myList.Count
在循环运行期间不变化,其访问权限已移出循环的条件语句,因此不再在每次循环迭代开始时执行它。
这两个版本的执行时间显示了从这一特定代码片段中减少 75% 方法调用开销的真正优势。在现代台式机上运行 100,000 次的情况下:
为了方便开发者,Unity 为数据类型提供了许多“简单”常量。但是,鉴于上述情况,必须注意:这些常量通常是以返回常量值的「属性」来实现的。
Vector3.zero 的属性内容如下所示:
get { return new Vector3(0,0,0); }
Quaternion.identity 非常相似:
get { return new Quaternion(0,0,0,1); }
虽然访问这些属性的成本与它们周围的执行代码相比小的多,但它们每帧执行数千次(或更多次)时,可产生一定的影响。
对于简单的原始类型,请改用 const
值。Const
值在编译时内联 - 对 const
变量的引用将替换为其值。
注意:因为对 const
变量的每个引用都替换为其值,所以不建议声明长字符串或其他大型数据类型 const
。否则,由于最终二进制代码中的所有重复数据,将导致不必要地增加最终二进制文件的大小。
当 const
不适合时,应使用 static readonly
变量。在有些项目中,Unity 的内置简单属性即使替换成了 static readonly
变量,也会使性能略有改善。
简单方法比较棘手。函数声明一次然后就可以在其他地方重用它,绝对是很有用的。但是,在紧凑内部循环中,可能有必要打破美观编码规则,选择“手动内联”某些代码。
有些方法可以彻底淘汰了,例如,Quaternion.Set
、Transform.Translate
或 Vector3.Scale
。这些方法执行非常简单的操作,可以用简单的赋值语句替换。
对于更复杂的方法,应权衡「手动内联的性能提升」与「维护性能更佳的代码的长期成本」之间的关系。
来源:
一般优化 - Unity 手册有多少原因导致性能问题,就有多少种不同的方式来优化代码。通常,强烈建议开发者在尝试应用 CPU 优化之前对其应用程序进行性能分析。不过,还是存在几种普遍适用的简易 CPU 优化方式。https://docs.unity3d.com/cn/2019.4/Manual/BestPracticeUnderstandingPerformanceInUnity7.html
特别优化 - Unity 手册