Мои Уведомления
Привет, !
Мой Аккаунт Мои Финансы Мои Подписки Мои Настройки Выход
Руководство API скрипты

Рекомендации по сбору мусора

Сборка мусора выполняется автоматически, но этот процесс требует значительного количества процессорного времени.

Автоматическое управление памятью C# снижает риск утечек памяти и других ошибок программирования по сравнению с другими языками программирования, такими как C++, где вы должны вручную отслеживать и освобождать всю выделенную память.

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

Временные распределения

Приложение обычно выделяет временные данные в управляемую кучу в каждом кадре; однако это может повлиять на производительность приложения. Например:

  • Если программа выделяет один килобайт (1 КБ) временной памяти для каждого кадра и работает со скоростью 60 кадров в секунду, частота, с которой отображаются последовательные кадры в запущенной игре. Подробнее
    См. в Словарь
    , то он должен выделять 60 килобайт временной памяти в секунду. В течение минуты это добавляет до 3,6 МБ памяти, доступной сборщику мусора.
  • Вызов сборщика мусора раз в секунду негативно влияет на производительность. Если сборщик мусора запускается только один раз в минуту, ему приходится очищать 3,6 МБ, распределенных по тысячам отдельных выделений, что может привести к значительному увеличению времени сборки мусора.
  • Операции загрузки влияют на производительность. Если ваше приложение генерирует много временных объектов во время тяжелой операции загрузки ресурсов, и Unity ссылается на эти объекты до завершения операции, сборщик мусора не может освободить эти временные объекты. Это означает, что управляемая куча должна расширяться, даже несмотря на то, что Unity освобождает множество содержащихся в ней объектов через некоторое время.

Чтобы обойти это, постарайтесь максимально уменьшить количество часто управляемых выделений кучи: в идеале до 0 байтов на кадр или как можно ближе к нулю.

Повторно используемые пулы объектов

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

Например, неоптимально создавать экземпляр нового объекта-снаряда из Prefabтипа актива, который позволяет хранить GameObject с компонентами и свойствами. Префаб действует как шаблон, из которого вы можете создавать новые экземпляры объектов в сцене. Подробнее
Просматривайте в Словарь
при каждом увольнении. Вместо этого вы можете рассчитать максимальное количество снарядов, которые могут существовать одновременно во время игры, и создать массив объектов правильного размера, когда игра впервые входит в сцену игрового процессаСцена содержит окружение и меню вашей игры. Думайте о каждом уникальном файле сцены как об уникальном уровне. В каждой сцене вы размещаете свое окружение, препятствия и декорации, по сути проектируя и создавая свою игру по частям. Подробнее
См. в Словарь
. Для этого:

  • Начните со всех снарядов GameObjectsФундаментальный объект в сценах Unity, который может представлять персонажей, реквизит, декорации, камеры, путевые точки и многое другое. Функциональность GameObject определяется прикрепленными к нему компонентами. Подробнее
    See in Словарь
    отключен.
  • При выстреле снаряда выполните поиск в массиве, чтобы найти первый неактивный снаряд в массиве, переместите его в нужное положение и сделайте игровой объект активным.
  • Когда снаряд будет уничтожен, снова установите GameObject в неактивное состояние.

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

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

using System.Collections.Generic; using UnityEngine; public class ExampleObjectPool : MonoBehaviour { public GameObject PrefabToPool; public int MaxPoolSize = 10; private Stack inactiveObjects = new Stack(); void Start() { if (PrefabToPool != null) { for (int i = 0; i < MaxPoolSize; ++i) { var newObj = Instantiate(PrefabToPool); newObj.SetActive(false); inactiveObjects.Push(newObj); } } } public GameObject GetObjectFromPool() { while (inactiveObjects.Count > 0) { var obj = inactiveObjects.Pop(); if (obj != null) { obj.SetActive(true); return obj; } else { Debug.LogWarning("Found a null object in the pool. Has some code outside the pool destroyed it?"); } } Debug.LogError("All pooled objects are already in use or have been destroyed"); return null; } public void ReturnObjectToPool(GameObject objectToDeactivate) { if (objectToDeactivate != null) { objectToDeactivate.SetActive(false); inactiveObjects.Push(objectToDeactivate); } } }

Повторяющаяся конкатенация строк

Строки в C# являются неизменяемымиВы не можете изменить содержимое неизменяемого (доступного только для чтения) пакета. Это противоположность mutable. Большинство пакетов являются неизменяемыми, включая пакеты, загруженные из реестра пакетов или по URL-адресу Git.
См. в Словарь
ссылочные типы. Ссылочный тип означает, что Unity размещает их в управляемой куче и подвергает сборке мусора. Неизменяемость означает, что однажды созданная строка не может быть изменена; любая попытка изменить строку приводит к созданию совершенно новой строки. По этой причине по возможности следует избегать создания временных строк.

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

// Bad C# script example: repeated string concatenations create 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; } } }

Метод, возвращающий значение массива

Иногда бывает удобно написать метод, который создает новый массив, заполняет его значениями и затем возвращает его. Однако, если этот метод вызывается повторно, каждый раз выделяется новая память.

В следующем примере кода показан пример метода, который создает массив при каждом вызове:

// 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; } } }

Этот код заменяет существующее содержимое массива новыми значениями. Этот рабочий процесс требует, чтобы вызывающий код выполнял начальное выделение массива, но при вызове функция не генерирует никакого нового мусора. Затем массив можно повторно использовать и повторно заполнить случайными числами при следующем вызове этого метода без каких-либо новых выделений в управляемой куче.

Коллекция и повторное использование массива

При использовании массивов или классов из System.Collection" (например, списки или словари), эффективно повторно использовать или объединять выделенную коллекцию или массив. Классы коллекций предоставляют метод Clear, который удаляет значения коллекции, но не освобождает память, выделенную коллекции.

Это полезно, если вы хотите выделить временные «вспомогательные» коллекции для сложных вычислений. Следующий пример кода демонстрирует это:

// Bad C# script example. This Update method allocates a new List every frame. void Update() { List nearestNeighbors = new List(); findDistancesToNearestNeighbors(nearestNeighbors); nearestNeighbors.Sort(); // … use the sorted list somehow … }

В этом примере кода список ближайших соседей выделяется один раз на кадр для сбора набора точек данных.

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

// Good C# script example. This method re-uses the same List every frame. List m_NearestNeighbors = new List(); void Update() { m_NearestNeighbors.Clear(); findDistancesToNearestNeighbors(NearestNeighbors); m_NearestNeighbors.Sort(); // … use the sorted list somehow … }

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

Замыкания и анонимные методы

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

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

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

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

// Good C# script example: using an anonymous method to sort a list. // This sorting method doesn’t create garbage List 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 listOfNumbers = getListOfRandomNumbers(); int desiredDivisor = getDesiredDivisor(); listOfNumbers.Sort( (x, y) => (int)x.CompareTo((int)(y/desiredDivisor)) );

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

Чтобы убедиться, что в замыкание передаются правильные значения, C# создает анонимный класс, который может сохранять переменные внешней области, необходимые замыканию. Копия этого класса создается, когда замыкание передается методу Sort, и эта копия инициализируется значением требуемого целого числа Divisor.

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

Бокс

Боксирование — один из наиболее распространенных источников непреднамеренного выделения временной памяти в проектах Unity. Это происходит, когда переменная типа значения автоматически преобразуется в ссылочный тип. Чаще всего это происходит при передаче примитивных переменных со значением (таких как int и float) в методы объектного типа. При написании кода C# для Unity следует избегать упаковки.

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

int x = 1; object y = new object(); y.Equals(x);

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

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

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

Боксирование появляется в трассировках ЦП как вызовы одного из нескольких методов, в зависимости от используемой серверной части сценария. Они принимают одну из следующих форм, где — это имя класса или структуры, а — это количество аргументов:

::Box(…) Box(…) _Box(…)

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

API Unity со значениями массива

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

Например, следующий код без необходимости создает четыре копии массива вершин на итерацию цикла. Распределение происходит каждый раз при доступе к свойству .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); } }

Лучший способ сделать это – создать список вершин, который кэшируется и повторно используется между кадрами, а затем использовать Mesh.GetVertices для заполнения при необходимости.

// Best C# script example: create one copy of the vertices array // and work with that. List m_vertices = new List(); 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); } }

Несмотря на то, что однократный доступ к ресурсу не влияет на производительность ЦП, повторяющиеся обращения в рамках замкнутых циклов приводят к проблемам с производительностью ЦП. Повторные обращения расширяют управляемую кучу.

Эта проблема распространена на мобильных устройствах, потому что API Input.touches ведет себя аналогично описанному выше. Также часто проекты содержат код, подобный приведенному ниже, где выделение происходит каждый раз при доступе к свойству .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]; // … }

To improve this, you can configure your code to hoist the array allocation out of the loop condition:

// 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]; // … }

В следующем примере кода предыдущий пример преобразуется в Touch API без выделения ресурсов:

// 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 свойства.

Альтернативные нераспределяющие API

У некоторых API Unity есть альтернативные версии, которые не вызывают выделения памяти. Вы должны использовать их, когда это возможно. В следующей таблице показан небольшой набор распространенных API-интерфейсов с выделением памяти и их нераспределяющих альтернатив. Список не является исчерпывающим, но в нем должны быть указаны типы API, на которые следует обратить внимание.

Распределение API Альтернатива API без выделения
Physics.RaycastAll Physics.RaycastNonAlloc
Animator.parameters Animator.parameterCount and Animator.GetParameter
Renderer.sharedMaterials Renderer.GetSharedMaterials

Повторное использование пустого массива

Некоторые группы разработчиков предпочитают возвращать пустые массивы вместо null, когда метод, возвращающий массив, должен возвращать пустой набор. Этот шаблон кодирования распространен во многих управляемых языках, особенно в C# и Java.

Как правило, при возврате массива нулевой длины из метода более эффективно возвращать предварительно выделенный статический экземпляр массива нулевой длины, чем многократно создавать пустые массивы.

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

Вы можете отблагодарить автора, за перевод документации на русский язык. ₽ Спасибо
Руководство Unity 2021.3