Оптимизация производительности загрузки: Понимание конвейера асинхронной загрузки

Никто не любит загрузочные экраны. Знаете ли вы, что можно быстро настроить параметры Async Upload Pipeline (AUP), чтобы значительно улучшить время загрузки? В этой статье подробно описано, как сетки и текстуры загружаются через AUP. Это понимание может помочь вам значительно ускорить время загрузки - в некоторых проектах производительность была увеличена более чем в 2 раза!
Читайте далее, чтобы узнать, как AUP работает с технической точки зрения и какие API следует использовать, чтобы получить максимальную отдачу от него.
Последняя, наиболее оптимальная реализация конвейера загрузки активов доступна в бета-версии 2018.3.
Для начала давайте подробно рассмотрим, в каких случаях используется AUP и как происходит процесс загрузки.
До версии 2018.3 AUP работал только с текстурами. Начиная с бета-версии 2018.3, AUP теперь загружает текстуры и сетки, но есть и исключения. Текстуры с поддержкой чтения/записи, сетки с поддержкой чтения/записи или сжатые, не будут использовать AUP. (Обратите внимание, что в потоковой передаче мипмапов текстур, которая появилась в версии 2018.2, также используется AUP).
В процессе сборки объект текстуры или сетки записывается в сериализованный файл, а большие двоичные данные (данные текстуры или вершин) записываются в сопутствующий файл .resS. Эта схема применяется как к данным игрока, так и к пакетам активов. Разделение объектных и двоичных данных позволяет ускорить загрузку сериализованного файла (который обычно содержит небольшие объекты), а также упростить загрузку больших двоичных данных из файла .resS. Когда объект текстуры или сетки десериализован, он отправляет команду в очередь команд AUP. После выполнения этой команды данные текстуры или сетки будут загружены в GPU, и объект можно будет интегрировать в основной поток.

В процессе загрузки большие двоичные данные из файла .resS считываются в кольцевой буфер фиксированного размера. Попав в память, данные загружаются на GPU с разбивкой по времени в потоке рендеринга. Размер кольцевого буфера и длительность временного среза - это два параметра, которые вы можете изменить, чтобы повлиять на поведение системы.
Конвейер Async Upload Pipeline имеет следующий процесс для каждой команды:
1. Подождите, пока в кольцевом буфере не освободится необходимая память.
2. Считывание данных из исходного файла .resS в выделенную память.
3. Выполните постобработку (декомпрессия текстур, генерация коллизий сетки, исправление ошибок для каждой платформы и т.д.).
4. Загрузка в потоке рендеринга с разбивкой по времени
5. Освобождение памяти Ring Buffer.
Несколько команд могут выполняться одновременно, но все они должны выделять необходимую им память из одного и того же общего кольцевого буфера. Когда кольцевой буфер заполнится, новые команды будут ожидать; это ожидание не приводит к блокировке основного потока и не влияет на частоту кадров, оно просто замедляет процесс асинхронной загрузки.
Ниже приводится краткое описание этих воздействий:

Чтобы в полной мере воспользоваться преимуществами AUP в 2018.3, есть три параметра, которые можно настроить во время выполнения этой системы:
- QualitySettings.asyncUploadTimeSlice - Количество времени в миллисекундах, затрачиваемое на загрузку текстур и данных сетки в потоке рендеринга для каждого кадра. Когда выполняется операция асинхронной загрузки, система выполнит два временных среза такого размера. Значение по умолчанию - 2 мс. Если это значение слишком мало, вы можете стать узким местом при загрузке текстур/мешей на GPU. Слишком большое значение, с другой стороны, может привести к задержке частоты кадров.
- QualitySettings.asyncUploadBufferSize - Размер кольцевого буфера в мегабайтах. Когда временной срез загрузки происходит каждый кадр, мы хотим быть уверены, что у нас достаточно данных в кольцевом буфере, чтобы использовать весь временной срез. Если кольцевой буфер слишком мал, временной отрезок загрузки будет сокращен. В версии 2018.2 по умолчанию было установлено значение 4 МБ, но в версии 2018.3 оно увеличилось до 16 МБ.
- QualitySettings.asyncUploadPersistentBuffer - Введен в 2018.3, этот флаг определяет, будет ли кольцевой буфер выгрузки деаллоцирован после завершения всех ожидающих чтений. Выделение и удаление этого буфера может часто приводить к фрагментации памяти, поэтому его следует оставить по умолчанию (true). Если вам действительно нужно освобождать память, когда вы не загружаетесь, вы можете установить это значение на false.
Эти настройки можно изменить через API сценариев или через меню QualitySettings.

Давайте рассмотрим рабочую нагрузку с большим количеством текстур и сеток, загружаемых через конвейер Async Upload Pipeline с использованием стандартного временного интервала 2 мс и кольцевого буфера размером 4 МБ. Поскольку мы загружаемся, мы получаем 2 временных фрагмента на каждый кадр рендеринга, поэтому время загрузки должно составлять 4 миллисекунды. Если посмотреть на данные профилировщика, то мы используем всего около 1,5 миллисекунды. Мы также видим, что сразу после загрузки выполняется новая операция чтения, поскольку память в кольцевом буфере свободна. Это признак того, что необходимо увеличить кольцевой буфер.

Давайте попробуем увеличить Ring Buffer и, поскольку мы находимся на экране загрузки, неплохо бы увеличить временной интервал загрузки. Вот как выглядит кольцевой буфер объемом 16 МБ и 4-миллисекундный временной срез:

Теперь мы видим, что почти все время потока рендеринга уходит на загрузку, и лишь небольшое время между загрузками - на рендеринг кадра.
Ниже приведено время загрузки примера рабочей нагрузки с различными временными интервалами загрузки и размерами Ring Buffer. Тесты проводились на MacBook Pro, 2,8 ГГц Intel Core i7 под управлением OS X El Capitan. Скорость загрузки и скорость ввода/вывода на разных платформах и устройствах различны. Рабочая нагрузка представляет собой подмножество проекта-образца Viking Village, который мы используем для внутреннего тестирования производительности. Из-за того, что загружаются и другие объекты, мы не можем точно определить выигрыш в производительности при различных значениях. Однако в данном случае можно с уверенностью сказать, что загрузка текстур и сетки происходит как минимум в два раза быстрее при переходе от настроек 4MB/2MS к настройкам 16MB/4MS.
Экспериментирование с этими параметрами дает следующие результаты.

Чтобы оптимизировать время загрузки для этого конкретного проекта-образца, мы должны настроить параметры следующим образом:
Неизвестный тип блока "codeBlock", укажите для него сериализатор в свойстве `serializers.types`.
Общие рекомендации по оптимизации скорости загрузки текстур и сеток:
- Выберите наибольший фрагмент QualitySettings.asyncUploadTimeSlice, который не приводит к выпадению кадров.
- Во время экранов загрузки временно увеличьте QualitySettings.asyncUploadTimeSlice.
- Используйте профилировщик для изучения использования временного среза. В профилировщике временной срез будет отображаться как AsyncUploadManager.AsyncResourceUpload. Увеличьте QualitySettings.asyncUploadBufferSize, если ваш временной срез используется не полностью.
- При большем размере QualitySettings.asyncUploadBufferSize все будет загружаться быстрее, поэтому, если вы можете позволить себе память, увеличьте его до 16 или 32 МБ.
- Оставьте QualitySettings.asyncUploadPersistentBuffer установленным в true, если у вас нет веских причин для сокращения использования памяти во время выполнения без загрузки.
Q: Как часто в потоке рендеринга будет происходить загрузка с нарезкой по времени?
- Загрузка с временными интервалами будет происходить один раз за кадр рендеринга или дважды во время асинхронной загрузки. VSync влияет на этот конвейер. Пока поток рендеринга ждет VSync, вы можете заниматься загрузкой. Если вы работаете с частотой кадров 16 мс, а затем один кадр затягивается, скажем, до 17 мс, то в итоге вы будете ждать vsync 15 мс. В целом, чем выше частота кадров, тем чаще будут происходить временные срезы загрузки.
Q: Что загружается через AUP?
- Текстуры, не поддерживающие чтение/запись, загружаются через AUP.
- Начиная с версии 2018.2, текстурные мипмапы передаются через AUP.
- Начиная с версии 2018.3, сетки также можно загружать через AUP, если они не сжаты и не включены в режим чтения/записи.
Q: Что делать, если кольцевой буфер недостаточно велик для хранения загружаемых данных (например, очень большой текстуры)?
- Команды загрузки, размер которых превышает размер кольцевого буфера, будут ждать, пока кольцевой буфер не будет полностью использован, после чего кольцевой буфер будет перераспределен, чтобы вместить большое распределение. После завершения загрузки кольцевой буфер будет перераспределен до первоначального размера.
Q: Как работают API синхронной загрузки? Например, Resources.Load, AssetBundle.LoadAsset и т. д.
- Синхронные вызовы загрузки используют AUP и, по сути, блокируют основной поток до тех пор, пока не завершится операция асинхронной загрузки. Тип используемого API для загрузки не имеет значения.
Мы всегда рады обратной связи. Сообщите нам о своем мнении в комментариях или на форуме бета-версии Unity 2018.3!