Главная » Хабрахабр » [Перевод] Оптимизация рендеринга сцены из диснеевского мультфильма «Моана». Часть 3

[Перевод] Оптимизация рендеринга сцены из диснеевского мультфильма «Моана». Часть 3

image

Сегодня мы рассмотрим ещё два места, в которых pbrt тратит много времени при парсинге сцены из диснеевского мультфильма «Моана». Посмотрим, удастся ли и здесь улучшить производительность. На этом мы закончим с тем, что разумно делать в pbrt-v3. Ещё в одном посте я буду разбираться с тем, насколько далеко мы можем зайти, если откажемся от запрета на внесение изменений. При этом исходный код будет слишком отличаться от системы, описанной в книге Physically Based Rendering.

Оптимизация самого парсера

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

Формат файлов сцен pbrt парсить довольно просто: если не учитывать закавыченных строк, токены разделяются пробелами, а грамматика очень прямолинейна (никогда не возникает потребности заглядывать вперёд дальше, чем на один токен), но собственный парсер — это всё равно тысяча строк кода, которые нужно написать и отладить. Я наконец-то собрался с силами и реализовал написанный вручную токенизатор и парсер для сцен pbrt. На этом этапе я был абсолютно уверен, что всё сделано верно.
Я старался, чтобы новая версия была как можно более эффективной, по возможности подвергая входные файлы mmap() и пользуясь новой реализацией std::string_view из C++17 для минимизации создания копий строк из содержимого файла. Мне помогло то, что его можно было протестировать на множестве сцен; после исправления очевидных сбоев я продолжал работу, пока мне не удалось отрендерить в точности те же изображения, что и раньше: не должно возникать никаких различий в пикселях по причине замены парсера. Кроме того, поскольку в предыдущих трассировках много времени уходило на strtod(), я писал функцию parseNumber() с особой аккуратностью: одноразрядные целые числа и обычные целые числа обрабатываются по отдельности, а в стандартном случае, когда pbrt компилируется для использования 32-битных float, применял strtof() вместо strtod()1.

Я никак не мог узнать заранее, будет ли всё время на написание новой версии потрачено впустую, пока не завершил её и не добился её правильной работы. В процессе создания реализации нового парсера я немного боялся того, что старый парсер будет быстрее: в конце концов, flex и bison разрабатываются и оптимизируются уже много лет.

Благодаря новому парсеру время запуска снизилось до 13 мин 21 с, то есть ускорилось ещё в 1,5 раза! К моей радости, собственный парсер оказался огромной победой: обобщённость flex и bison так сильно снижала производительность, что новая версия легко их обгоняла. Она всегда была головной болью, особенно под Windows, где у большинства людей они по умолчанию не установлены. Дополнительный бонус заключался в том, что из системы сборки pbrt теперь можно было убрать всю поддержку flex и bison.

Управление состоянием графики

После значительного ускорения работы парсера всплыла новая раздражающая деталь: на этом этапе примерно 10% времени настройки тратилось на функции pbrtAttributeBegin() и pbrtAttributeEnd(), и бОльшую часть этого времени занимало выделение и освобождение динамической памяти. Во время первого запуска, занимавшего 35 минут, на эти функции уходило всего около 3% времени выполнения, поэтому на них можно было не обращать внимания. Но при оптимизации всегда так: когда начинаешь избавляться от больших проблем, мелкие становятся важнее.

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

Для хранения копий объектов GraphicsState в стеке сохранённых состояний графики используется std::vector. Состояние графики хранится в структуре с неожиданным названием… GraphicsState. Взглянув на члены GraphicsState, можно предположить источник проблем — три std::map, от имён до экземпляров текстур и материалов:

struct GraphicsState { // ... std::map<std::string, std::shared_ptr<Texture<Float>>> floatTextures; std::map<std::string, std::shared_ptr<Texture<Spectrum>>> spectrumTextures; std::map<std::string, std::shared_ptr<MaterialInstance>> namedMaterials;
};

Исследуя эти файлы сцен, я обнаружил, что большинство случаев сохранения и восстановления состояния графики выполняется в этих строках:

AttributeBegin ConcatTransform [0.981262 0.133695 -0.138749 0.000000 -0.067901 0.913846 0.400343 0.000000 0.180319 -0.383420 0.905800 0.000000 11.095301 18.852249 9.481399 1.000000] ObjectInstance "archivebaycedar0001_mod"
AttributeEnd

Другими словами, здесь выполняется обновление текущего преобразования и создание экземпляра объекта; в содержимое этих std::map никаких изменений не вносится. Создание их полной копии — выделение узлов красно-чёрного дерева, увеличение счётчиков ссылок общих указателей, выделение пространства и копирование строк — почти всегда является лишней тратой времени. Всё это освобождается при восстановлении предыдущего состояния графики.

Изменение оказалось не особо сложным, но снизило время запуска больше чем на минуту, что дало нам 12 мин 20 с обработки перед началом рендеринга — снова ускорение в 1,08 раза. Я заменил каждый из этих map указателем std::shared_ptr на map и реализовал подход copy-on-write, при котором копирование внутри блока begin/end атрибута происходит только тогда, когда его содержимое должно быть изменено.

А как насчёт времени рендеринга?

Внимательный читатель заметит, что пока я ничего не говорил о времени рендеринга. К моему удивлению, оно оказалось вполне терпимым даже «из коробки»: pbrt может рендерить изображения сцен кинематографического качества с несколькими сотнями сэмплов на пиксель на двенадцати ядрах процессора за период в два-три часа. Например, это изображение, одно из самых медленных, отрендерилось за 2 ч 51 мин 36 с:

Дюны из «Моаны», отрендеренные pbrt-v3 с разрешением 2048x858 при 256 сэмплах на пиксель. Общее время рендеринга на инстансе Google Compute Engine с 12 ядрами / 24 потоками с частотой 2 ГГц и последней версией pbrt-v3 равнялось 2 ч 51 мин 36 с.

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

Эти соотношения похожи на показатели более простых сцен, поэтому на первый взгляд ничего очевидно проблемного здесь нет. При профилировании выяснилось, что примерно 60% времени рендеринга тратилось на пересечения лучей с объектами (большинство операций выполнялось при обходе BVH), а 25% тратилось на поиск текстур ptex. (Однако я уверен, что Embree сможет оттрассировать эти лучи за чуть меньшее время.)

Обычно я вижу, что на рендеринг тратится 1400% ресурсов ЦП, по сравнению с идеалом в 2400% (на 24 виртуальных ЦП в Google Compute Engine). К сожалению, параллельная масштабируемость не так хороша. Очень вероятно, свой вклад вносит то, что pbrt-v3 не вычисляет в трассировщике лучей разность лучей для непрямых лучей; в свою очередь, такие лучи всегда получают доступ к самому детализированному MIP-уровню текстур, что не очень полезно для кэширования текстур. Похоже, что проблема связана с конфликтами при блокировках в ptex, но подробнее я её пока не исследовал.

Заключение (для pbrt-v3)

Исправив управление состоянием графики, я упёрся в предел, после которого дальнейший прогресс без внесения в систему значительных изменений становился неочевидным; всё оставшееся занимало много времени и мало относилось к оптимизации. Поэтому на этом я остановлюсь, по крайней мере, в том, что касается pbrt-v3.

Большее того, благодаря умной работе с кэшем преобразований использование памяти снизилось 80 ГБ до 69 ГБ. В целом прогресс был серьёзным: время запуска перед рендерингом снизилось с 35 минут до 12 мин 20 с, то есть общее ускорение составило 2,83 раза. Все эти изменения доступны уже сейчас, если вы синхронизируетесь с последней версией pbrt-v3 (или если вы это сделали в течение последних нескольких месяцев.) И мы приходим к пониманию того, насколько мусорной является память Primitive для этой сцены; мы выяснили, как сэкономить ещё 18 ГБ памяти, но не реализовали это в pbrt-v3.

Вот, на что тратятся эти 12 мин 20 с после всех наших оптимизаций:

Функция / операция

Процент времени выполнения

Построение BVH

34%

Парсинг (например, strtof())

21%

strtof()

20%

Кэш преобразований

7%

Считывание файлов PLY

6%

Выделение динамической памяти

5%

Обращение преобразований

2%

Управление состоянием графики

2%

Прочее

3%

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

Однако стоит быть аккуратным с выбором замен, которые протестированы не очень тщательно: парсинг float-значений — это один из тех аспектов, в надёжности которых программист должен быть полностью уверен. В какой-то момент я посмотрю, существуют ли более быстрые реализации strtof(); pbrt использует только то, что предоставляет ему система.

Мы можем добавить поддержку двоичного кодирования входных файлов pbrt (возможно, по аналогии с подходом RenderMan), однако я испытываю относительно этой идеи смешанные чувства; возможность открытия и изменения файлов описания сцен в текстовом редакторе довольно полезна, и я беспокоюсь, что иногда двоичное кодирование будет сбивать с толку студентов, использующих pbrt в процессе обучения. Также привлекательным выглядит дальнейшее снижение нагрузки на парсер: у нас по-прежнему есть 17 ГБ текстовых входных файлов для парсинга. Это один из тех случаев, когда правильное решение для pbrt может отличаться от решений для коммерческого рендерера производственного уровня.

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

В следующей статье, мы рассмотрим аспекты, которые могут ещё больше повысить производительность, если мы допустим внесение в pbrt более радикальных изменений.

Примечание

  1. На системе Linux, в которой я выполнял тестирование, strtof() не быстрее, чем strtod(). Примечательно, что на OS X strtod() примерно в два раза быстрее, что совершенно нелогично. Исходя из практических соображений, я продолжил использовать strtof().

Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Новые инструменты разработки с LEGO Education — от Microsoft, MIT и не только

Из шести кубиков LEGO размером 2х4 можно собрать 915 миллионов различных комбинаций. Но в LEGO Education конструирование из кубиков — это лишь часть дела. Даже проекты для дошкольников здесь включают в себя программирование, пусть и в простейшей форме. Мы стремимся ...

Используем 54 ФЗ на благо домашней бухгалтерии

Когда очередная редакция Федерального Закона номер 54 «О применении контрольно-кассовой техники» вступила в силу, большая часть населения встретила её негативно. Примерно вот так (18+, содержит нецензурную лексику) Я был одним из немногих, кто прыгал, хлопал в ладоши и вообще радовался ...