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

Введение

In an ideal world, computers would run at infinite speed. The only limit to what we could achieve would be our imagination. However, in the real world, it's all too easy to produce software that will bring even the fastest computer to its knees.

Thus, designing games and other software is a compromise between what we would like to be possible, and what we can realistically achieve while maintaining good performance.

To achieve the best results, we have two approaches:

  • Work faster.

  • Work smarter.

And preferably, we will use a blend of the two.

Smoke and mirrors

Part of working smarter is recognizing that, in games, we can often get the player to believe they're in a world that is far more complex, interactive, and graphically exciting than it really is. A good programmer is a magician, and should strive to learn the tricks of the trade while trying to invent new ones.

The nature of slowness

To the outside observer, performance problems are often lumped together. But in reality, there are several different kinds of performance problems:

  • A slow process that occurs every frame, leading to a continuously low frame rate.

  • An intermittent process that causes "spikes" of slowness, leading to stalls.

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

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

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.