Искать

Оптимальная работа со структурами данных C# и Unity API

Last updated: December 2018

What you will get from this page: solutions for working optimally with Unity APIs and data structures. Challenges covered include recursive iteration with Particle System APIs; frequent memory allocation; APIs that return Arrays; choosing incorrect data structures, and too much overhead from the misuse of dictionaries.

A lot of these tips will introduce additional complexity, which results in higher maintenance overhead and the possibility for more bugs. Therefore, profile your code first before applying some of these solutions.

All of these tips are from the Unite talk, Выжимаем все из Unity: советы по увеличению производительности. For updated tips from Ian that take into account the evolution of Unity’s architecture to data-oriented design, see Развитие наилучших практик.

Unity APIs

Проблема: рекурсивная итерация с API системы частиц:

При вызове одного из главных API системы частиц, например методов Start, Stop, IsAlive(), по умолчанию происходит их рекурсивная итерация по всем дочерним элементам иерархии системы частиц:

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

Решение:

В этих API есть параметр withChildren со значением true по умолчанию. Для того чтобы избежать рекурсии, параметру withChildren следует присвоить значение false: в таком случае будет изменена только та система частиц, к которой осуществляется вызов.

Проблема: множество систем частиц, которые запускаются и останавливаются одновременно

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

Решение:

Создайте класс MonoBehaviour, который будет кэшировать список систем частиц, вызывая список GetComponent во время инициализации. Затем, когда нужно будет изменить эти системы частиц, вызывайте методы Start, Stop, и так далее по очереди, не забыв задать значение false в параметре withChildren.

Проблема: частое выделение места в оперативной памяти

Когда цикл C# замыкается на закрытую переменную, среда исполнения C# должна разместить ссылку в динамической памяти для отслеживания переменной. В Unity версий с 5.4 по 2017.1 есть несколько API систем частиц, которые при вызове используют замыкания. В этих версиях Unity все вызовы методов Stop и Simulate будут приводить к распределению места в динамической памяти даже при остановленной системе частиц.

Решение:

Все API систем частиц, как и большинство общедоступных API Unity, по своей сути являются надстройками на языке C# над внутренними функциями, выполняющими работу API. Некоторые из внутренних функций написаны на чистом C#, но большинство из них сводится к вызовам ядра движка Unity, написанного на C++. В случае API систем частиц названия всех внутренних API для удобства начинаются с сочетания «Internal_», после которого следуют имена общедоступных функций, например:

work-optimally-with-Unity-APIs-table-graph

...и так далее.

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

work-optimally-with-Unity-APIs-functions-signatures

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

Проблема: API, возвращающих массивы

Каждый раз, когда вы обращаетесь к API Unity, который возвращает массив, в памяти появляется новая копия этого массива. Это происходит как при использовании функций, возвращающих массивы, так и при использовании атрибутов, возвращающих массивы. Самые распространенные примеры — Mesh.vertices, при обращении к которому в памяти появляется новая копия массива вершин трехмерной сетки, и Input.touches, который возвращает массив всех прикосновений пользователя к экрану в данном кадре.

Решение:

У большинства API Unity есть версии, не выделяющие места в памяти, которые и следует использовать. Например, вместо Input.touches можно использовать Input.GetTouch и Input.touchCount. В версии 5.3 и всех последующих версиях Unity есть API обращений к Physics, которые не требуют выделения места в памяти. Для двумерных приложений есть аналогичные API обращений к Physics2D. Узнать подробнее об API физики Unity, не требующих выделения места в памяти, можно здесь.

И наконец, если вы используете GetComponents или GetComponentsInChildren, то теперь есть версии, которые принимают общий, типизированный список. Эти API наполняют список результатами вызова метода GetComponents. Память при этом все же выделяется: если результатов вызова GetComponents будет больше, чем число элементов в списке, то список потребуется расширить. Но если вы многократно используете или объединяете списки, то это даст вам как минимум сокращение числа распределений памяти во время работы приложения.

Оптимальное использование структур данных

Проблема: выбор неправильных структур данных

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

Решение:

Индексирование в массив или список требует совсем немного ресурсов на добавление значения, а сама операция выполняется за одно и то же время. Следовательно, случайное индексирование или итерация по массиву или списку дает совсем небольшой прирост к потреблению ресурсов. Таким образом, если вам нужно проверять список элементов в каждом кадре — используйте массивы или списки.

Если вы постоянно добавляете или убираете значения из структуры, то вам наверняка будет удобнее использовать словарь или Hashset (подробнее об этом ниже).

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

Подробнее о Hashset и словарях: в основе обеих структур данных лежат хеш-таблицы. Помните, что каждая хеш-таблица имеет несколько корзин, которые представляют собой список всех значений с определенным хеш-кодом. В C# этот код создается методом GetHashCode. Набор значений в конкретной корзине обычно гораздо меньше общего числа элементов в хешсете или в словаре, поэтому время выполнения операции добавления или удаления значения из корзины будет ближе к постоянному, нежели добавление или удаление случайного элемента из списка или массива. Точная разница зависит от емкости вашей хеш-таблицы и от числа элементов, которые в ней хранятся.

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

Проблема: повышенное потребление ресурсов при злоупотреблении словарями

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

Решение:

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

Где-то на отметке 14:25 доклада Иэн дает совет о том, когда и как использовать словари с ключами InstanceID для уменьшения потребления ресурсов.

Проблема: а если вопросов несколько?

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

Распространенный пример — Update Manager. Это шаблон архитектурного программирования, где объект (обычно MonoBehaviour) рассылает обратные вызовы Update различным системам игры. Когда системам нужно получить вызов Update, они подписываются на него через объект Update Manager. В такой системе необходима незатратная итерация, а также операция вставки и проверка дубликатов с постоянным временем выполнения.

Решение: использовать две структуры данных

  • Для итерации используйте список или массив.
  • Перед изменением списка используйте хешсет (или другой набор индексируемых данных), чтобы убедиться в том, что добавляемый или удаляемый элемент действительно существует.
  • Если необходимо удалить элемент, используйте связный список или интрузивную реализацию списка (помните, что это увеличивает потребление памяти и ресурсов на итерацию).
Дополнительные ресурсы

Мы очень хотим знать, нравится ли вам наш контент.

Да, хочу больше Нет, могло быть и лучше
Согласен

Мы используем cookie-файлы, чтобы вам было удобнее работать с нашим веб-сайтом. Подробные сведения смотрите на странице политики обработки cookie-файлов.