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

Специальные оптимизации

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

Многомерные и зубчатые массивы

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

ПРИМЕЧАНИЯ:

  • Это массивы массивов, объявленные как type[x][y] вместо type[x,y ].)

  • Это можно обнаружить, проверив IL, созданный при доступе к многомерному массиву с помощью ILSpy или аналогичных инструментов.)

При профилировании в Unity 5.3 100 полностью последовательных итераций над трехмерным массивом 100 x 100 x 100 дали следующие значения времени, которые были усреднены по 10 запускам теста:

Тип массива Общее время (100 итераций)
One-Dimensional Array 660 ms
Jagged Arrays 730 ms
Multidimensional Array 3470 ms

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

Как показано выше, стоимость вызова дополнительной функции значительно превышает стоимость, связанную с использованием некомпактной структуры памяти.

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

Объединение систем частиц

При объединении систем частиц помните, что они потребляют не менее 3500 байт памяти. Потребление памяти увеличивается в зависимости от количества модулей, активированных в Системе частицКомпоненте, моделирующем текучие объекты, такие как жидкости, облака и пламя. путем создания и анимации большого количества небольших 2D-изображений в сцене. Подробнее
См. в Словарь
. Эта память не освобождается при деактивации систем частиц; Он высвобождается только при их уничтожении.

Начиная с Unity 5.3, большинством настроек системы частиц теперь можно управлять во время выполнения. Для проектов, которые должны объединять большое количество различных эффектов частиц, может быть более эффективным извлекать параметры конфигурации систем частиц в класс или структуру носителя данных.

Когда требуется эффект частиц, пул «общих» эффектов частиц может предоставить необходимый объект эффекта частиц. Затем данные конфигурации можно применить к объекту для достижения желаемого графического эффекта.

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

Менеджеры обновлений

Внутри Unity отслеживает списки объектов, заинтересованных в своих обратных вызовах, таких как Update, FixedUpdate и Позднее обновление. Они поддерживаются как навязчиво связанные списки, чтобы гарантировать, что обновления списков происходят в постоянное время. MonoBehaviours добавляются в эти списки или удаляются из них, когда они включены или отключены соответственно.

Хотя удобно просто добавлять соответствующие обратные вызовы к MonoBehaviours, которым они требуются, это становится все более неэффективным по мере роста количества обратных вызовов. Вызов обратных вызовов управляемого кода из машинного кода сопряжен с небольшими, но значительными накладными расходами. Это приводит как к ухудшению времени кадра при вызове большого количества методов для каждого кадра, так и к ухудшению времени создания экземпляров при создании экземпляров Prefabресурса. тип, который позволяет хранить GameObject вместе с компонентами и свойствами. Префаб действует как шаблон, из которого вы можете создавать новые экземпляры объектов в сцене. Подробнее
См. в Словарь
, который содержит большое количество MonoBehaviours (ПРИМЕЧАНИЕ: Стоимость создания экземпляра связана с накладными расходами на производительность при вызове обратных вызовов Awake и OnEnable для каждого компонента в префабе.).

Когда количество объектов MonoBehaviours с обратными вызовами для каждого кадра достигает сотен или тысяч, целесообразно удалить эти обратные вызовы и вместо этого присоединить MonoBehaviours (или даже стандартные объекты C#) к синглтону глобального менеджера. Затем синглтон глобального менеджера может распространять Update, LateUpdate и другие обратные вызовы для заинтересованных объектов. Это имеет дополнительное преимущество, позволяя коду разумно отписываться от обратных вызовов, когда они в противном случае не выполнялись бы, тем самым сокращая количество функций, которые должны вызываться в каждом кадре.

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

void Update() { if(!someVeryRareCondition) { return; } // … some operation … }

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

Использование делегатов C# в диспетчере обновлений

Заманчиво использовать простые делегаты C# для реализации этих обратных вызовов. Однако реализация делегата C# оптимизирована для низкой скорости подписки и отказа от подписки, а также для небольшого количества обратных вызовов. Делегат C# выполняет полную глубокую копию списка обратных вызовов каждый раз при добавлении или удалении обратного вызова. Большие списки обратных вызовов или большое количество обратных вызовов с подпиской или отказом от подписки в течение одного кадра приводят к скачку производительности во внутреннем методе Delegate.Combine.

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

Загрузка управления потоком

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

Приоритетом основного потока и графического потока является ThreadPriority.Normal. Любые потоки с более высоким приоритетом вытесняют основные/графические потоки и вызывают сбои частоты кадров, в то время как потоки с более низким приоритет не делать. Если потоки имеют приоритет, эквивалентный основному потоку, ЦП пытается предоставить потокам равное время, что обычно приводит к заиканию частоты кадров, если несколько фоновых потоков выполняют тяжелые операции, такие как распаковка AssetBundle.

В настоящее время этот приоритет можно контролировать в трех местах.

Во-первых, приоритет по умолчанию для вызовов загрузки активов, таких как Resources.LoadAsync и AssetBundle.LoadAssetAsync, берется из параметр Application.backgroundLoadingPriority. Как задокументировано, этот вызов также ограничивает количество времени, которое основной поток тратит на интеграцию активов (ПРИМЕЧАНИЕ: Большинство типов ресурсов Unity должны быть «интегрированы» в основной поток. Во время интеграции завершается инициализация Актива и выполняются определенные потокобезопасные операции. Это включает в себя сценарии обратных вызовов, таких как обратные вызовы Awake. Дополнительную информацию см. в руководстве «Управление ресурсами», чтобы ограничить влияние загрузки ресурсов на время кадра.

Во-вторых, каждая асинхронная операция загрузки объекта, а также каждый запрос UnityWebRequest возвращает объект AsyncOperation для мониторинга и управления операцией. Этот объект AsyncOperation предоставляет свойство priority, которое можно использовать для настройки приоритета отдельной операции.

Наконец, объекты WWW, например объекты, возвращенные вызовом WWW.LoadFromCacheOrDownload, предоставляют threadPriority свойство. Важно отметить, что объекты WWW не используют параметр Application.backgroundLoadingPriority автоматически в качестве значения по умолчанию — объекты WWW по умолчанию всегда имеют значение ThreadPriority.Normal. .

Важно отметить, что внутренние системы, используемые для распаковки и загрузки данных, различаются между этими API. Resources.LoadAsync и AssetBundle.LoadAssetAsync управляются внутренней системой Unity PreloadManager, которая управляет собственными потоками загрузки и выполняет собственное ограничение скорости. UnityWebRequest использует собственный выделенный пул потоков. WWW создает совершенно новый поток каждый раз, когда создается запрос.

Хотя все другие загрузочные механизмы имеют встроенную систему очередей, в WWW ее нет. Вызов WWW.LoadFromCacheOrDownload для очень большого количества сжатых пакетов AssetBundle порождает эквивалентное количество потоков, которые затем конкурируют с основным потоком за процессорное время. Это может легко привести к скачкам частоты кадров.

Поэтому при использовании WWW для загрузки и распаковки AssetBundles рекомендуется устанавливать соответствующее значение для threadPriority каждого создаваемого объекта WWW.

Массовое движение объектов и группы отсечения

Как упоминалось в разделе "Управление преобразованиями", перемещение больших иерархий преобразований связано с относительно высокой нагрузкой на ЦП из-за распространения сообщений об изменениях. Однако в реальных средах разработки зачастую невозможно свернуть иерархию до скромного количества GameObjectsфундаментального объекта в сценах Unity. , который может представлять персонажей, реквизит, декорации, камеры, путевые точки и многое другое. Функциональность GameObject определяется прикрепленными к нему компонентами. Подробнее
См. в Словарь
.

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

Обе эти проблемы можно аккуратно решить с помощью API, впервые представленного в Unity 5.1: CullingGroups.

Вместо прямого управления большой группой игровых объектов в сцене измените систему, чтобы управлять параметрами Vector3 группы BoundingSpheres в CullingGroup. Каждая BoundingSphere служит авторитетным репозиторием для положения одной игровой сущности в мировом пространстве и получает обратные вызовы, когда сущность перемещается рядом или внутри усеченной пирамиды основной камерыКомпонент, который создает изображение определенной точки обзора в вашей сцене. Вывод либо рисуется на экране, либо фиксируется в виде текстуры. Подробнее
См. в Словарь
. Затем эти обратные вызовы можно использовать для активации/деактивации кода или компонентов (например, аниматоров), управляющих поведением, которое должно выполняться только тогда, когда объект виден.

Уменьшение накладных расходов на вызов метода

Библиотека строк C# представляет собой отличный пример стоимости добавления дополнительных вызовов методов к коду простой библиотеки. В разделе о встроенных строковых API String.StartsWith и String.EndsWith было упомянуто, что замены, закодированные вручную работают в 10–100 раз быстрее, чем встроенные методы, даже если нежелательное приведение локали было подавлено.

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

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

Рассмотрите следующие два простых метода.

Пример 1:

int Accum { get; set; } Accum = 0; for(int i = 0; i < myList.Count; i++) { Accum += myList[i]; }

Пример 2:

int accum = 0; int len = myList.Count; for(int i = 0; i < len; i++) { accum += myList[i]; }

Оба метода вычисляют сумму всех целых чисел в универсальном List C#. Первый пример немного более «современный C#», поскольку он использует автоматически сгенерированное свойство для хранения своих значений данных.

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

Пример 1:

int Accum { get; set; } Accum = 0; for(int i = 0; i < myList.Count; // call to List::getCount i++) { Accum // call to set_Accum += // call to get_Accum myList[i]; // call to List::get_Value }

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

  • myList.Count вызывает метод get для свойства Count
  • Методы get и set свойства Accum должны быть называется
  • get, чтобы получить текущее значение Accum, чтобы его можно было передать в операцию сложения
  • установить, чтобы присвоить результат операции сложения Accum
  • Оператор [] вызывает метод списка get_Value для получения значения элемента по определенному индексу в списке.

Пример 2:

int accum = 0; int len = myList.Count; for(int i = 0; i < len; i++) { accum += myList[i]; // call to List::get_Value }

В этом втором примере вызов get_Value остается, но все остальные методы либо исключены, либо больше не выполняются один раз за итерацию цикла.

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

    < /li>
  • Поскольку предполагается, что myList.Count не меняется во время выполнения цикла, его доступ был перемещен за пределы условного оператора цикла, поэтому он больше не выполняется в начале каждой итерации цикла.

Время для двух версий показывает истинное преимущество удаления 75 % накладных расходов на вызов метода из этого конкретного фрагмента кода. При запуске 100 000 раз на современном настольном компьютере:

  • Пример 1 требует 324 миллисекунды для выполнения
  • Пример 2 требует 128 миллисекунд для выполнения

Основная проблема здесь заключается в том, что Unity очень мало выполняет встраивание методов, если оно вообще есть. Даже в IL2CPPРазработанный Unity сервер сценариев, который можно использовать в качестве альтернативы Mono при создании проектов для некоторых платформ. Подробнее
См. в Словарь
, многие методы в настоящее время не встроены должным образом. Особенно это касается свойств. Кроме того, виртуальные методы и методы интерфейса вообще не могут быть встроены.

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

Простые свойства

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

Свойство Vector3.zero выглядит следующим образом:

get { return new Vector3(0,0,0); }

Quaternion.identity очень похож:

get { return new Quaternion(0,0,0,1); }

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

Для простых примитивных типов вместо этого используйте значение const. Значения Const встраиваются во время компиляции — ссылка на переменную const заменяется ее значением.

Примечание. Поскольку каждая ссылка на переменную const заменяется ее значением, не рекомендуется объявлять длинные строки или другие большие типы данных < класс кода = "моно"> константа . Это излишне увеличивает размер конечного двоичного файла из-за дублирования данных в конечном коде инструкции.

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

Простые методы

Простые методы сложнее. Чрезвычайно полезно иметь возможность объявить функциональность один раз и повторно использовать ее в другом месте. Однако в тесных внутренних циклах может потребоваться отступить от передовой практики написания кода и вместо этого «встроить» определенный код вручную.

Некоторые методы можно полностью исключить. Рассмотрим Quaternion.Set, Transform.Translate или Vector3.Scale. Они выполняют очень тривиальные операции и могут быть заменены простыми операторами присваивания.

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

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