Version: 6000.3
语言: 中文
使用具有可重用内存的编码模式
测试代码

优化阵列

以下页面概述了在使用数组时提高代码性能的示例。

将数组作为参数传递给方法

有时编写一个方法可能会很方便,该方法创建一个新数组,用值填充数组,然后返回它。但是,如果重复调用此方法,则每次都会分配新内存。

以下示例代码显示了每次调用时都会创建一个数组的方法示例:

// Bad C# script example: Every time the RandomList method is called it
// allocates a new array
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;
    }
}

避免每次都分配内存的一种方法是利用数组是引用类型的事实。可以修改作为参数传递到方法中的数组,结果在方法返回后保留。为此,您可以按如下方式配置示例代码:

// Good C# script example: This version of method is passed an array to fill
// with random values. The array can be cached and re-used to avoid repeated
// temporary allocations
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属性被访问:

// Bad C# script example: this loop create 4 copies of the vertices array per iteration
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);   
    }
}

您可以将此代码重构为单个数组分配,而不管循环迭代次数如何。为此,请将代码配置为在循环之前捕获顶点数组:

// Better C# script example: create one copy of the vertices array
// and work with that
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);   
    }
}

执行此作的最佳方法是保持List的顶点,在帧之间缓存和重用,然后使用Mesh.GetVertices在需要时填充它。

// Best C# script example: create one copy of the vertices array
// and work with that.
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.touchesAPI 的行为与前面的示例类似。项目通常包含类似于以下内容的代码,其中每次.touches属性被访问:

// Bad C# script example: Input.touches returns an array every time it’s accessed
for ( int i = 0; i < Input.touches.Length; i++ ) {
   Touch touch = Input.touches[i];

    // …
}

为了改善这一点,您可以将代码配置为将数组分配提升到循环条件之外:

// Better C# script example: Input.touches is only accessed once here
Touch[] touches = Input.touches;

for ( int i = 0; i < touches.Length; i++ ) {

   Touch touch = touches[i];

   // …
}

以下代码示例将前面的示例转换为 allocation-freeTouch应用程序接口:

// BEST C# script example: Input.touchCount and Input.GetTouch don’t allocate at all.
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.parameterCountAnimator.GetParameter
Renderer.sharedMaterials Renderer.GetSharedMaterials

通常,如果该方法返回数组,则通常有一个非分配版本的 API,您可以使用它来将数组传递给该版本。

使用零长度数组的静态实例

一些开发团队更喜欢返回空数组而不是null当数组值方法需要返回空集时。这种编码模式在许多托管语言中很常见,尤其是 C# 和 Java。

从方法返回零长度数组时,返回零长度数组的预分配静态实例比重复创建空数组更有效。

对大型数组使用本机数组或其他本机容器

Unity 的垃圾回收器是一个启发式垃圾回收器,这意味着它将任何大小为指针的字段视为指针。垃圾回收器检查它遇到的每个指针和引用。

当您使用大型数组(包含超过 10,000 个元素)时,垃圾回收器可能会将大型数组解释为需要检查的一长串指针,这会增加内存压力并减慢垃圾回收器的速度。

脚本 VM 还必须为大型数组分配大块托管堆空间,这增加了随机值看起来像托管堆块内存地址范围内有效地址的可能性。大型数组也是导致托管堆碎片化的一个重要因素。

如果需要分配这种长度的数组,请考虑使用Unity.Collections命名空间(包括NativeArray) 以及 Unity Collections 包中的数据结构。作为额外的好处,使用这些集合与作业系统和 Burst 兼容。

其他资源

使用具有可重用内存的编码模式
测试代码