Некоторое время назад я рассказывал о «Профилировании и отладке Django». После выступления я получил много вопросов (как лично, так и по email), с парой новых знакомых мы даже выбрались в бар, чтобы обсудить важные проблемы программирования за кружечкой отменного эля, со многими людьми я продолжаю общаться до сих пор.
Поскольку выступление вызвало живой интерес, а беседы с коллегами позволили мне переосмыслить некоторые моменты презентации и исправить достадные ляпы, я решил оформить доклад и свои мысли в виде статьи. Это позволит ознакомиться с темой гораздо большему кругу заинтересованных лиц, к тому же Хабр предоставляет из себя идеальную площадку для комментирования предложенного материала и общения с интересными собеседниками.
Материала много, статья получилась огромная, поэтому я решил разбить её на несколько частей:
- Введение и теория — зачем вообще нужно профилирование, различные подходы, инструменты и отличия между ними
- Ручное и статистическое профилирование — переходим к практике
- Событийное профилирование — инструменты и их применение
- Отладка — что делать, когда ничего не работает
Введение
В первую очередь необходимо разобраться с определениями. Читаем в Википедии:
Профилирование — сбор характеристик работы программы с целью их дальнейшей оптимизации.
Итак, для профилирования нам нужна работающая программа, причём работающая не совсем так, как нам хотелось бы: либо работающая слишком медленно, либо потребляющая слишком много ресурсов.
Какие же характеристики работы программы можно собирать?
- время выполнения отдельных строк кода (инструкций)
- количество вызовов и время выполнения отдельных функций
- дерево вызовов функций
- «hot spots» (участки кода, на которые приходится существенная доля исполненных инструкций)
- загрузку CPU и потребление памяти
- обращение к другим ресурсам компьютера (например, к файловым дескрипторам)
- и т.д. и т.п.
Конечно, не всегда есть смысл изучать проект подробно под микроскопом, разбирая каждую инструкцию и изучая всё досканально, но знать что, как и где мы можем посмотреть, полезно и нужно.
Давайте определимся с понятием «оптимизация». Википедия подсказывает нам, что:
Оптимизация — это модификация системы для улучшения её эффективности.
Понятие «эффективность» — очень расплывчатое, и напрямую зависит от поставленной цели, например, в одних случаях программа должна работать максимально быстро, в других можно пренебречь скоростью и гораздо важнее сэкономить оперативную память или другие ресурсы (такие, как диск). Как справедливо сказал Фредерик Брукс, «серебрянной пули не существует».
Очевидно, что оптимизацией программы можно заниматься бесконечно: в любом достаточно сложном проекте всегда найдётся узкое место, которое можно улучшить, поэтому важно уметь останавливаться вовремя. В большинстве случаев (исключения крайне редки и относятся, скорее, к фольклору, чем к реальной жизни) нет смысла тратить, скажем, три дня рабочего времени ради 5% выигрыша по скорости.
С другой стороны, как любил повторять Дональд Кнут: «Преждевременная оптимизация — это корень всех бед».
Какая статья про оптимизацию обходится без этой цитаты? Многие полагают, что её автор — Дональд Кнут, но сам Дональд утверждает, что впервые её произнёс Энтони Хоар. Энтони же отпирается и предлагает считать высказывание «всеобщим достоянием».
Важно чётко понимать, что именно нас не устраивает в работе программы и каких целей мы хотим достичь, но это не значит, что профилированием и последующей оптимизацией нужно заниматься тогда, когда всё начинает тормозить. Хороший программист всегда знает, как себя чувствует написанная им программа и прогнозирует её работоспособность в критических ситуациях (таких, как хабраэффект).
Хочу заметить ещё один немаловажный момент: часто оптимизация сопровождается значительным ухудшением читаемости кода. По возможности, старайтесь избегать этого, а в случае, если всё-таки пришлось написать менее читаемый код в критичных местах, не поленитесь и оставьте комментарий с подробным описанием его работы. Ваши коллеги скажут вам спасибо, да и вы сами посчитаете себя удивительно проницательным, когда вернётесь к этим строкам через некоторое время.
Я не буду больше затрагивать вопрос оптимизации, поскольку, как я сказал выше, всё очень сильно зависит от поставленной задачи и каждый случай нужно разбирать отдельно. Сосредоточимся на обнаружении проблемных участков программы.
Подходы к профилированию
Существует, по крайней мере, три подхода к профилированию:
- метод пристального взгляда
- ручное профилирование
- с использованием инструментов
С методом пристального взгляда (и родственным ему «методом тыка») всё понятно. Просто садимся перед текстовым редактором, открываем код и думаем, где может быть проблема, пробуем починить, смотрим на результат, откатываемся. И только в редких случаях (либо при высочайшей квалификации разработчика) метод оказывается действенным.
Достоинства и недостатки этого метода:
+ не требует особых знаний и умений
– сложно оценить трудозатраты и результат
Ручное профилирование удобно использовать, когда есть обоснованное предположение об узких местах и требуется подтвердить или опровергнуть гипотезу. Либо если нам, в отличие от первого метода, нужно получить численные показатели результатов нашей оптимизации (например, функция выполнялась за 946 милисекунд, стала отрабатывать за 73 милисекунды, ускорили код в 13 раз).
Суть этого метода в следующем: перед выполнением спорного участка программы сохраняем в переменную текущее системное время (с точностью до микросекунд), а после заново получаем текущее время и вычитаем из него значение сохранённой переменной. Получаем (с достаточной для нас погрешностью) время выполнения анализируемого кода. Для достоверного результата повторяем N раз и берём среднее значение.
Достоинства и недостатки этого метода:
+ очень простое применение
+ ограниченно подходит для продакшена
– вставка «чужеродного» кода в проект
– использование возможно не всегда
– никакой информации о программе, кроме времени выполнения анализируемого участка
– анализ результатов может быть затруднительным
Профилирование с помощью инструментов помогает, когда мы (по тем или иным причинам) не знаем, отчего программа работает не так, как следует, либо когда нам лень использовать ручное профилирование и анализировать его результаты. Подробнее об инструментах в следующем разделе.
Должен заметить, что независимо от выбранного подхода, главным инструментом разработчика остаётся его мозг. Ни одна программа (пока(?)) не скажет:
Эй, да у тебя в строке 619 файла project/app.py ерунда написана! Вынеси-ка вызов той функции из цикла и будет тебе счастье. И ещё, если ты используешь кэширование, и перепишешь функцию calculate на Си, тогда быстродействие увеличится в среднем в 18 раз!
Какие бывают инструменты
Инструменты бывают двух видов (на самом деле вариантов классификации и терминологии гораздо больше, но мы ограничимся двумя):
- статистический (statistical) профайлер
- событийный (deterministic, event-based) профайлер
К сожалению, я так и не смог придумать красивого названия на русском языке для «детерминистического» профайлера, поэтому я буду использовать слово «событийный». Буду благодарен, если кто-нибудь поправит меня в комментариях.
Большинство разработчиков знакомы только с событийными профайлерами, и большой неожиданностью для них оказывается тот факт, что статистический профайлер появился первым: в начале семидесятых годов прошлого столетия программисты компьютеров IBM/360 и IBM/370 ставили прерывание по таймеру, которое записывало текущее значение Program status word (PSW). Дальнейший анализ сохранённых данных позволял определить проблемные участки программы.
Первый событийный профайлер появился в конце тех же семидесятых годов, это была утилита ОС Unix prof, которая показывала время выполнения всех функций программы. Спустя несколько лет (1982 год) появилась утилита gprof, которая научилась отображать граф вызовов функций.
Статистический профайлер
Принцип работы статистического профайлера прост: через заданные (достаточно маленькие) промежутки времени берётся указатель на текущую выполняемую инструкцию и сохраняет эту информацию («семплы») для последующего изучения. Выглядит это так:
видно, функция bar()выполнялась почти в два с половиной раза дольше, чем функции foo(), baz() и какая-то безымянная инструкция.
Один из недостатков статистического профайлера заключается в том, что для получения адекватной статистики работы программы нужно провести как можно большее (в идеале — бесконечное) количество измерений с как можно меньшим интервалом. Иначе некоторые вызовы вообще могут быть не проанализированы:
например, из рисунка видно, что безымянная функция не попала в выборку.
Так же сложно оценить реальное время работы анализируемых функций. Рассмотрим ситуацию, когда функция foo() выполняется достаточно быстро, но вызывается очень часто:
и ситуацию, когда функция foo() выполняется очень долго, но вызывается лишь один раз:
результат работы статистического профайлера будет одинаковым в обоих случаях.
Тем не менее, с поиском самых «тяжёлых» и «горячих» мест программы статистический профайлер справляется великолепно, а его минимальное влияние на анализируемую программу (и, как следствие, пригодность к использованию в продакшене) перечёркивает все минусы. К тому же Python позволяет получить полный stacktrace для кода при семплировании и его анализ позволяет получать более полную и подробную картину.
Достоинства и недостатки статистического профайлера:
+ можно пускать в продакшен (влияние на анализируемую программу минимально)
– получаем далеко не всю информация о коде (фактически только «hot spots»)
– возможно некорректное интерпретирование результата
– требуется длительное время для сбора адекватной статистики
– мало инструментов для анализа
Событийный профайлер
Событийный профайлер отслеживает все вызовы функций, возвраты, исключения и замеряет интервалы между этими событиями. Измеренное время (вместе с информацией о соответствующих участках кода и количестве вызовов) сохраняется для дальнейшего анализа.
Самый важный недостаток таких профайлеров прямо следует из принципа их работы: поскольку мы вмешиваемся в анализируемую программу на каждом шагу, процесс её выполнения может (и будет) сильно отличаться от «обычных» условий работы (прям как в квантовой механике). Так, например, в некоторых случаях возможно замедление работы программы в два и более раз. Конечно, в продакшен выпускать такое можно только в случае отчаяния и полной безысходности.
И тем не менее плюсы перевешивают минусы, иначе не было бы такого огромного разнообразия различных инструментов. Просмотр результатов в удобном интерфейсе с возможностью анализа времени выполнения и количества вызовов каждой строки программы многого стоят, граф вызовов помогает обнаружить недостатки в используемых алгоритмах.
Достоинства и недостатки событийных профайлеров:
+ не требуется изменения кода
+ получаем всю информаци о работе программы
+ огромное количество инструментов
– в некоторых случаях профайлер меняет поведение программы
– очень медленно
– практически непригодно для продакшена
В следующей статье мы на практике разберём ручное профилирование и статистические профайлеры. Оставайтесь на связи =)