Unity 6.3
0 онлайн 95 гостей 3 в системе
Вход
Программирование в Unity Шаг 139 из 210

Управление справочными типами

Поскольку все ненулевые объекты ссылочных типов и все упакованные объекты значимых типов в управляемом коде должны размещаться в управляемой куче, эти объекты могут вызывать проблемы с производительностью приложения. В следующих разделах описаны подходы к повышению производительности кода.

Избегать повторяющегося сцепления строк

Строки в C# являются неизменными типами ссылок. Unity выделяет типы ссылок в управляемом стеке, и они подлежат сбору мусора. Поскольку строки неизменны и не могут изменяться после создания, избегайте создания временных строк, когда это возможно.

В следующем примере код объединяет массив строк в одну строку. Каждый раз, когда в цикл добавляется новая строка, предыдущее содержимое переменной результата становится избыточным, и код выделяет целую новую строку.

// 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.Text, что означает, что нет необходимости в конкатенации строк. Код все еще должен преобразовать значение партитуры в строку, но это улучшение по сравнению с предыдущими версиями:

// 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 []) метод в 2000 году TMPro.TMP_Text класс из UGUI пакет. SetText метод позволяет использовать и повторно использовать char массив, чтобы построить счет цифра за цифрой и обновить значения в char массив без необходимости использования строки.

Дополнительные сведения о рекомендациях по работе со строками в C# см. в документации Майкрософт по Рекомендации по сравнению строк в .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. Boxing является одним из наиболее распространенных источников непреднамеренного временного выделения памяти в проектах Unity. Это чаще всего происходит при передаче примитивных переменных типа значение (таких как int и float) методам типа объект.

В этом примере целое число в x закрыто, чтобы его можно было передать методу object.Equals, потому что метод Equals на объекте требует, чтобы ему был передан объект.

int x = 1;

object y = new object();

y.Equals(x);

C# IDEs и компиляторы не выводят предупреждения о boxing, даже если boxing приводит к непреднамеренным выделениям памяти. Это потому, что C# предполагает, что небольшие временные выделения эффективно обрабатываются генерационными сборщиками мусора и пулами памяти, чувствительными к размеру выделения.

Хотя распределитель управляемой памяти Unity использует разные пулы для малых и больших выделений памяти, сборщик мусора Unity не относится к поколенческим и поэтому не может эффективно освобождать небольшие частые временные выделения, создаваемые упаковкой значимых типов.

Идентификация бокса

Boxing появляется в треках CPU как вызов одного из нескольких методов, в зависимости от используемого скриптового бэк-энда. Они принимают одну из следующих форм, где <example class> — это имя класса или структуры, а является числом аргументов:

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

Чтобы найти boxing, вы также можете искать в выводе декомпилятора или просмотрщика IL, например, инструмента просмотра IL, встроенного в ReSharper, или декомпилятора dotPeek. Инструкция IL — это box.

Избегайте модификатора params

Методы, которые перечисляют свои факультативные параметры, как с params модифицировать выделить массив для параметров, которые вы передаёте в них. Если это возможно, используйте переопределения этих методов, которые не зависят от этого модификатора.

Дополнительные ресурсы