Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

General optimization tips

Введение

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

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

Для достижения наилучших результатов у нас есть два подхода:

  • Работайте быстрее.

  • Работайте разумнее.

И желательно, мы будем использовать смесь этих двух.

Smoke and mirrors

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

Природа медлительности

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

  • Медленный процесс, происходящий в каждом кадре и приводящий к постоянно низкой частоте кадров.

  • Прерывистый процесс, вызывающий "всплески" замедления, приводящие к остановкам.

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

Каждый из них раздражает пользователя, но по-разному.

Measuring performance

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

Существует несколько методов измерения производительности, в том числе:

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

Ограничения

Профилировщики процессора часто являются основным методом измерения производительности. Однако они не всегда рассказывают всю историю.

  • Узкие места часто возникают на GPU, "в результате" инструкций, отданных CPU.

  • Скачки могут возникать в процессах операционной системы (вне Godot) "в результате" инструкций, используемых в Godot (например, динамическое выделение памяти).

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

  • Возможно, вам придется решать проблемы производительности, возникающие на оборудовании, к которому у вас нет доступа.

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

Детективная работа

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

Hypothesis testing

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

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

Это может привести к еще одной гипотезе: определяет ли размер спрайта падение производительности?

  • Вы можете проверить это, оставив все без изменений, но изменив размер спрайта, и измерив производительность.

Profilers

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

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

For more info about using Godot's built-in profiler, see The Profiler.

Принципы

Дональд Кнут сказал:

Программисты тратят огромное количество времени, думая или беспокоясь о скорости некритичных частей своих программ, и эти попытки добиться эффективности на самом деле оказывают сильное негативное влияние при отладке и сопровождении. Мы должны забыть о небольшой эффективности, скажем, в 97% случаев: преждевременная оптимизация - корень всех зол. И все же мы не должны упускать свои возможности в этих критических 3%.

Важные сообщения:

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

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

Если мы можем оптимизировать определенный фрагмент кода, это не обязательно означает, что мы должны. Знание того, когда и когда не стоит оптимизировать, является отличным навыком для развития.

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

Performant design

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

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

Инкрементное проектирование

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

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

The optimization process

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

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

Процесс происходит следующим образом:

  1. Профиль / Выявление узких мест.

  2. Оптимизируйте узкое место.

  3. Переход к первому шагу.

Оптимизация узких мест

Некоторые профилировщики даже скажут вам, какая часть функции (какие обращения к данным, вычисления) замедляет работу.

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

Если алгоритмы и данные хороши, часто можно внести небольшие изменения в процедуры, которые повышают производительность. Например, можно перенести некоторые вычисления за пределы циклов или преобразовать вложенные циклы for в не вложенные циклы. (Это возможно, если вы заранее знаете ширину или высоту двумерного массива.)

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

Приложение

Математика узких мест

Пословица "цепь сильна лишь настолько, насколько сильно ее самое слабое звено" напрямую относится к оптимизации производительности. Если ваш проект проводит 90% времени в функции A, то оптимизация A может оказать огромное влияние на производительность.

A: 9 ms
Everything else: 1 ms
Total frame time: 10 ms
A: 1 ms
Everything else: 1ms
Total frame time: 2 ms

В данном примере улучшение этого узкого места A в 9 раз уменьшает общее время кадра на 5 раз, увеличивая количество кадров в секунду на 5 раз.

Однако если что-то еще работает медленно и также является узким местом в вашем проекте, то такое же улучшение может привести к менее значительным результатам:

A: 9 ms
Everything else: 50 ms
Total frame time: 59 ms
A: 1 ms
Everything else: 50 ms
Total frame time: 51 ms

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

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

CPU: 9 ms
GPU: 50 ms
Total frame time: 50 ms
CPU: 1 ms
GPU: 50 ms
Total frame time: 50 ms

В этом примере мы снова сильно оптимизировали CPU, но кадровое время не улучшилось, потому что мы упираемся в GPU.