Unity的数组优化
原文链接:Unity - Manual: Optimizing arrays
数组优化
以下页面概述了在使用数组时提高代码性能的示例。
将数组作为参数传递给方法
有时编写一个创建新数组、填充数组值然后返回的方法会很方便。然而,如果这个方法被重复调用,每次都会分配新的内存。
以下示例代码展示了一个每次调用都会创建数组的方法:
// 糟糕的 C# 脚本示例:每次调用 RandomList 方法都会分配一个新数组
using UnityEngine;
using System.Collections;
public class ExampleScript : MonoBehaviour {
float[] RandomList(int numElements) {
var result = new float[numElements];
for (int i = 0; i < numElements; i++) {
result[i] = Random.value;
}
return result;
}
}
避免每次分配内存的一种方法是利用数组是引用类型这一特性。你可以修改作为参数传入方法的数组,方法返回后结果仍然保留。你可以按如下方式配置示例代码:
// 良好的 C# 脚本示例:此版本的方法接收一个数组来填充随机值。
// 该数组可以被缓存和重复使用,以避免重复的临时分配
using UnityEngine;
using System.Collections;
public class ExampleScript : MonoBehaviour {
void RandomList(float[] arrayToFill) {
for (int i = 0; i < arrayToFill.Length; i++) {
arrayToFill[i] = Random.value;
}
}
}
这段代码用新值替换数组的现有内容。这个工作流程要求调用代码执行数组的初始分配,但函数在被调用时不会产生任何新的垃圾。下次调用此方法时,数组可以被重复使用并重新填充随机数,而不会在托管堆上产生新的分配。
避免重复访问返回数组的 Unity API
数组意外分配的一个原因是重复访问返回数组的 Unity API。所有返回数组的 Unity API 在每次被访问时都会创建数组的新副本。如果你的代码比必要更频繁地访问返回数组的 Unity API,可能会影响应用程序的性能。
例如,以下代码每次循环迭代创建顶点数组的四个副本。每次访问 .vertices 属性时都会发生分配:
// 糟糕的 C# 脚本示例:此循环每次迭代创建 4 个顶点数组副本
void Update() {
for(int i = 0; i < mesh.vertices.Length; i++) {
float x, y, z;
x = mesh.vertices[i].x;
y = mesh.vertices[i].y;
z = mesh.vertices[i].z;
// ...
DoSomething(x, y, z);
}
}
你可以重构这段代码,使其无论循环迭代多少次都只进行一次数组分配。为此,配置你的代码在循环之前捕获顶点数组:
// 较好的 C# 脚本示例:创建一份顶点数组副本并使用它
void Update() {
var vertices = mesh.vertices;
for(int i = 0; i < vertices.Length; i++) {
float x, y, z;
x = vertices[i].x;
y = vertices[i].y;
z = vertices[i].z;
// ...
DoSomething(x, y, z);
}
}
最优的做法是维护一个顶点列表,在帧之间缓存和重复使用,然后在需要时使用 Mesh.GetVertices 来填充它。
// 最佳的 C# 脚本示例:创建一份顶点数组副本并使用它
List<Vector3> m_vertices = new List<Vector3>();
void Update() {
mesh.GetVertices(m_vertices);
for(int i = 0; i < m_vertices.Length; i++) {
float x, y, z;
x = m_vertices[i].x;
y = m_vertices[i].y;
z = m_vertices[i].z;
// ...
DoSomething(x, y, z);
}
}
虽然访问一次分配数组的属性的 CPU 性能影响不高,但在紧循环中重复访问会创建 CPU 性能热点。重复访问会扩展托管堆。
这个问题在移动设备上很常见,因为 Input.touches API 的行为与前面的示例类似。项目中通常也包含类似以下的代码,每次访问 .touches 属性时都会发生分配:
// 糟糕的 C# 脚本示例:Input.touches 每次被访问都会返回一个数组
for ( int i = 0; i < Input.touches.Length; i++ ) {
Touch touch = Input.touches[i];
// …
}
为了改进这一点,你可以配置代码将数组分配提升到循环条件之外:
// 较好的 C# 脚本示例:Input.touches 在这里只被访问一次
Touch[] touches = Input.touches;
for ( int i = 0; i < touches.Length; i++ ) {
Touch touch = touches[i];
// …
}
以下代码示例将前面的示例转换为无分配的触摸 API:
// 最佳的 C# 脚本示例:Input.touchCount 和 Input.GetTouch 完全不分配内存
int touchCount = Input.touchCount;
for ( int i = 0; i < touchCount; i++ ) {
Touch touch = Input.GetTouch(i);
// …
}
注意:属性访问(Input.touchCount)保持在循环条件之外,以节省调用属性 get 方法的 CPU 开销。
无分配 API 的替代方案
某些 Unity API 有不导致内存分配的替代版本。你应该尽可能使用这些。下表包含分配型 API 及其无分配替代方案的示例:
表格
| 分配型 API | 无分配 API 替代方案 |
|---|---|
| Physics.RaycastAll | Physics.RaycastNonAlloc |
| Animator.parameters | Animator.parameterCount 和 Animator.GetParameter |
| Renderer.sharedMaterials | Renderer.GetSharedMaterials |
一般来说,如果方法返回数组,通常有一个无分配版本的 API 可以用来传递数组。
使用零长度数组的静态实例
某些开发团队更喜欢在数组值方法需要返回空集时返回空数组而不是 null。这种编码模式在许多托管语言中很常见,特别是 C# 和 Java。
当从方法返回零长度数组时,返回预分配的零长度数组静态实例比重复创建空数组更高效。
对大型数组使用原生数组或其他原生容器
Unity 的垃圾回收器是启发式垃圾回收器,这意味着它将任何指针大小的字段视为指针。垃圾回收器检查它遇到的每个指针和引用。
当你使用大型数组(超过一万个元素)时,垃圾回收器可能会将大型数组解释为需要检查的长指针列表,这会增加内存压力并减慢垃圾回收速度。
脚本虚拟机还必须为大型数组分配大块的托管堆空间,这增加了随机值看起来像托管堆块内存地址范围内有效地址的可能性。大型数组也是托管堆碎片化的主要因素。
如果你需要分配如此长度的数组,请考虑在 Unity 核心 API 中使用 Unity.Collections 命名空间中的集合类型(包括 NativeArray),以及 Unity Collections 包中的数据结构。作为额外的好处,使用这些集合与作业系统和 Burst 兼容。

浙公网安备 33010602011771号