Искать

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

Last updated: December 2018

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

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

Все эти советы взяты с доклада на Unite, Выжимаем все из Unity: советы по увеличению производительности. Актуализированные версии советов от Иэна с учетом перехода Unity на информационно-ориентированную архитектуру смотрите здесь: Развитие наилучших практик.

Unity API

Проблема: рекурсивная итерация с 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. В такой системе необходима незатратная итерация, а также операция вставки и проверка дубликатов с постоянным временем выполнения.

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

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

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

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