Pull to refresh

Прагматичный подход к производительности

Reading time7 min
Views2.3K
Original author: BitSquid
Является преждевременная оптимизация дорогой в ад? Или подход «потом исправим» превращает программистов из «специалистов» в презираемую всеми «школоту»?

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

1. Время программиста – конечный ресурс


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

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

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

2. Не недооценивайте силу простоты


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

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

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

Само собой, простота видна только для конкретного человека. Я считаю, что массивы просты. Я считаю, что типы данных POD просты. Я считаю, что блобы просты. Я не считаю, что классы с 12 уровнями наследования просты. Я не считаю, что классы, основанные на 8 разных политиках, просты. Я не считаю, что геометрическая алгебра проста.

3. Берите все возможное от шанса спроектировать систему


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

С таким подходом я полностью не согласен. И не потому, что я люблю производительность ради производительности, а из чисто прагматических интересов.

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

Обратно, если вы построите систему без оглядки на производительность, и займетесь «исправлениями» позже, все может оказаться гораздо сложнее. Если вам потребуется реорганизовать базовые структуры данных или ввести поддержку мультитрединга, модет оказаться, что вы перепишите всю систему с нуля. Только система уже будет в работе, и вы будет ограничены опубликованным API и связями с другими системами. К тому же, вы не можете допустить поломки проектов, которые используют систему. А так как к тому моменту пройдет несколько месяцев как вы (или ещё кто-то) написали код, вам придется начать вспоминать и понимать все те мысли, которые вошли в него. А все небольшие исправления и поправки, которые были внесены за время написания, скорее всего, будут утеряны. И вы начнете отладку с обновленным мешком багов.

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

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

Когда я проектирую систему, я делаю грубый набросок того, как часто вызываются участки кода за единицу времени, и формулирую требования к дизайну:
  • 1-10. Производительность неважна. Делай что угодно
  • 100. Убедись, что это O(n), ориентировано на данные и может быть кэшировано
  • 1000. Используй мультитрединг
  • 10000. Хорошо подумай, что тут делаешь

Так же есть несколько рекомендаций, которым я стараюсь следовать при написании новых систем:
  • Статические данные складывай в постоянные целые блоки памяти
  • Складывай динамические данные в смежных участках памяти
  • Экономь память
  • Массивы лучше сложных структур данных
  • Обращайся к памяти линейно (упрощает кэширование)
  • Убедись, что функции выполняются за время O(n)
  • Избегай обновлений типа «ничего не делаю» — лучше следи за активными объектами
  • Если система работает со многими объектами – обеспечь параллельный доступ к данным

На сегодняшний день я написал очень много систем в таком «стиле», и мне не требуется прикладывать усилия, чтобы следовать этим рекомендациям. И я убедился, что следуя им, я получаю пристойную базовую производительность. Данные рекомендации относятся к самым важным и легко достижимым моментам увеличения производительности: сложность алгоритма, доступ к памяти и параллелизм; и что самое главное – дают значительный прирост производительности при сравнительно небольших усилиях.

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

4. Используйте профилирования сверху-вниз для поиска узких мест


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

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


Скриншот (старого) профайлера BitSquid

Профилирование сверху-вниз подскажет вам, на чем стоит сконцентрировать усилия по оптимизации. Используете 60% времени на анимацию и 0.5% времени на интерфейс? Займитесь анимацией, это сработает, а интерфейс и копейки ломанной не стоит.

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

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

5. Используйте профилирование снизу-вверх для поиска низкоуровневых целей оптимизации


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

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

Например, если функция strcmp() отображается как горячая точка, то ваша программа очень плохо себя ведет, и её срочно надо поставить в угол и лишить сладкого.

Часто проявляющаяся горячая точка нашего кода – lua_Vexecute(). Что не удивительно. Это основная функция Lua VM, большой переключатель, который запускает большую часть кода Lua. Но нам это говорит, что низкоуровневая платформо-зависимая оптимизация этой функции может дать ощутимый выигрыш в производительности.

6. Избегайте синтетических тестов


Я не провожу много синтетических тестов таких, как запуск кода в цикле 10000 раз и измерения времени исполнения.

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

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

7. Оптимизация – это садоводство


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

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

Улучшать производительность тоже самое, что ухаживать за садом: посмотреть что все хорошо, прополоть сорняки, придумать как сделать жизнь растений лучше.

Задача сценаристов уронить движок. А задача программистов – поднять его обратно, гораздо более мощным. И в процессе этого противостояния, находится та точка, в которой игра сияет наиболее ярко.



Перевел .
Оригинал A Pragmatic Approach to Performance

Всех с наступающим! Пишите много хороших и быстрых программ!
Tags:
Hubs:
+59
Comments26

Articles

Change theme settings