Version: 6000.3
语言: 中文
针对托管内存优化代码
使用具有可重用内存的编码模式

参考类型管理

由于托管代码中的所有非 null 引用类型对象和所有装箱值类型对象都必须在托管堆上分配,因此这些对象可能是导致应用程序中性能问题的原因。以下部分概述了提高代码性能的方法。

避免重复串联

C# 中的字符串是不可变的引用类型。Unity 在托管堆上分配引用类型,它们受垃圾回收的约束。因为字符串是不可变 不能更改不可变(只读)包的内容。这与可变相反。大多数包都是不可变的,包括从包注册表或通过 Git URL 下载的包。
请参阅术语表
并且一旦创建就无法更改,请尽可能避免创建临时字符串。

以下示例代码将字符串数组组合为单个字符串。每次在循环中添加新字符串时,结果变量的先前内容都会变得多余,并且代码会分配一个全新的字符串。

// Bad C# script example: repeated string concatenations creates lots of
// temporary strings.
using UnityEngine;

public class ExampleScript : MonoBehaviour {
    string ConcatExample(string[] stringArray) {
        string result = "";

        for (int i = 0; i < stringArray.Length; i++) {
            result += stringArray[i];
        }

        return result;
    }

}

如果输入stringArray包含{ “A”, “B”, “C”, “D”, “E” },则此方法在堆上为以下字符串生成存储:

  • “A”
  • “AB”
  • “ABC”
  • “ABCD”
  • “ABCDE”

在此示例中,您只需要最后一个字符串,其他字符串是冗余分配。输入数组中的项越多,此方法生成的字符串就越多,每个字符串都比上一个字符串长。

如果您需要将大量字符串连接在一起,请使用 Mono 库的System.Text.StringBuilder类。先前脚本的改进版本如下所示:

// Good C# script example: StringBuilder avoids creating temporary strings,
// and only allocates heap memory for the final result string.
using UnityEngine;
using System.Text;

public class ExampleScript : MonoBehaviour {
    private StringBuilder _sb = new StringBuilder(16);

    string ConcatExample(string[] stringArray) {
        _sb.Clear();

        for (int i = 0; i < stringArray.Length; i++) {
            _sb.Append(stringArray[i]);
        }

        return _sb.ToString();
    }
}

重复串联不会对性能降低太多,除非经常调用,例如在每次帧更新时。以下示例每次分配新字符串Update,并生成垃圾回收器必须处理的连续对象流:

// Bad C# script example: Converting the score value to a string every frame
// and concatenating it with “Score: “ generates strings every frame.
using UnityEngine;
using UnityEngine.UI;

public class ExampleScript : MonoBehaviour {
    public Text scoreBoard;
    public int score;
    
    void Update() {
        string scoreText = "Score: " + score.ToString();
        scoreBoard.text = scoreText;
    }
}

为了防止这种持续的垃圾回收要求,您可以配置代码,以便文本仅在分数更改时更新:

// Better C# script example: the score conversion is only performed when the
// score has changed
using UnityEngine;
using UnityEngine.UI;

public class ExampleScript : MonoBehaviour {
    public Text scoreBoard;
    public string scoreText;
    public int score;
    public int oldScore;
    
    void Update() {
        if (score != oldScore) {
            scoreText = "Score: " + score.ToString();
            scoreBoard.text = scoreText;
            oldScore = score;
        }
    }
}

为了进一步改进这一点,您可以存储分数标题(显示“Score: ”) 和分数以两种不同的方式显示UI.Textobjects,这意味着不需要字符串串联。代码仍必须将分数值转换为字符串,但这是对以前版本的改进:

// Best C# script example: the score conversion is only performed when the
// score has changed, and the string concatenation has been removed
using UnityEngine;
using UnityEngine.UI;

public class ExampleScript : MonoBehaviour {
   public Text scoreBoardTitle;
   public Text scoreBoardDisplay;
   public string scoreText;
   public int score;
   public int oldScore;

   void Start() {
       scoreBoardTitle.text = "Score: ";
   }

   void Update() {
       if (score != oldScore) {
           scoreText = score.ToString();
           scoreBoardDisplay.text = scoreText;
           oldScore = score;
       }
   }
}

对于更优化的版本,您可以使用SetText(Char [])方法TMPro.TMP_TextSetText方法允许您使用和重用char数组逐个数字构建分数,并更新char数组,无需使用字符串。

有关在 C# 中使用字符串的最佳做法的详细信息,请参阅 Microsoft 的文档,了解比较 .NET 中字符串的最佳做法

避免闭包和匿名方法

通常,尽可能避免在 C# 中使用闭包。应尽量减少在性能敏感型代码中使用匿名方法方法引用,尤其是在按帧执行的代码中。

C# 中的方法引用是引用类型,因此它们在托管堆上分配。这意味着,如果将方法引用作为参数传递,则可能会创建临时分配。无论传递的方法是匿名方法还是预定义方法,都会发生此分配。

此外,当您将匿名方法转换为闭包时,将闭包传递给方法所需的内存量会大大增加。

这是一个代码示例,其中随机数字列表需要按特定顺序排序。这使用匿名方法来控制列表的排序顺序,并且排序不会创建任何分配。

// Good C# script example: using an anonymous method to sort a list. 
// This sorting method doesn’t create garbage

List<float> listOfNumbers = getListOfRandomNumbers();

listOfNumbers.Sort( (x, y) =>

(int)x.CompareTo((int)(y/2)) 

);

若要使此代码段可重用,可以将常量 2 替换为局部作用域中的变量:

// Bad C# script example: the anonymous method has become a closure,
// and now allocates memory to store the value of desiredDivisor
// every time it is called.

List<float> listOfNumbers = getListOfRandomNumbers();

int desiredDivisor = getDesiredDivisor();

listOfNumbers.Sort( (x, y) =>

(int)x.CompareTo((int)(y/desiredDivisor))

);

匿名方法现在需要访问超出其作用域的变量的状态,因此该方法已成为闭包。这desiredDivisor变量必须传递到闭包中,以便闭包的代码可以使用它。

为了确保将正确的值传递给闭包,C# 生成了一个匿名类,该类可以保留闭包所需的外部作用域变量。当闭包传递给Sort方法,并且副本使用desiredDivisor整数。

执行闭包需要实例化其生成类的副本,并且所有类都是 C# 中的引用类型。因此,执行闭包需要在托管堆上分配一个对象。

避免将值类型转换为引用类型

当值类型变量自动转换为引用类型时,这称为装箱。Boxing 是 Unity 项目中意外临时内存分配的最常见来源之一。这最常发生在传递原始值类型变量(例如intfloat) 转换为对象类型方法。

在此示例中,中的整数x被装箱,以便可以将其传递给object.Equals方法,因为Equals对象上的方法要求将对象传递给它。

int x = 1;

object y = new object();

y.Equals(x);

C# IDE 和编译器不会发出有关装箱的警告,即使装箱会导致意外的内存分配也是如此。这是因为 C# 假定小型临时分配由分代垃圾回收器和分配大小敏感的内存池有效处理。

虽然 Unity 的托管内存分配器确实使用不同的内存池进行小型和大型分配,但 Unity 的垃圾收集器不是分代的,因此它无法有效地清除装箱生成的小型、频繁的临时分配。

识别拳击

装箱在 CPU 跟踪中显示为对几个方法之一的调用,具体取决于正在使用的脚本后端。这些采用以下形式之一,其中<example class>是类或结构的名称,并且是多个参数:

<example class>::Box(…)
Box(…)
<example class>_Box(…)

要查找装箱,您还可以搜索反编译器或 IL 查看器的输出,例如 ReSharper 内置的 IL 查看器工具dotPeek 反编译器。IL 指令为box.

避免使用 params 修饰符

列出其可选参数的方法,如params修饰语为您传递到其中的参数分配一个数组。如果可用,请使用不依赖于该修饰符的这些方法的替代。

其他资源

针对托管内存优化代码
使用具有可重用内存的编码模式