Найти в Дзене

Профилирование асинхронного Python

Общие слова Профилирование приложений — это процесс анализа программы для определения её характеристик: времени выполнения различных частей кода и использования ресурсов. Основные этапы профилирования всегда более-менее одинаковы: А как вообще работает профилировщик? Детальному обзору будет посвящена отдельная статья, пока можно ограничится базовой классификацией: Основные типы узких мест в асинхронном Python-коде Для асинхронного кода существует небольшое количество специфических "узких мест", которые лучше перечислить заранее. Каждому типу сопоставим пример кода. Список допущений Блокирующие операции Последовательный вызов асинхронных задач Слишком частое переключение контекста Неравномерное распределение ресурсов В англоязычной литературе такой сценарий называется "Resource Starvation". Чрезмерный расход памяти Использование "scalene" для профилирования Почему scalene? Потому что этот инструмент позволяет профилировать и CPU, и GPU, и память; 10k+ звёзд на гитхабе, проект активно ра
Оглавление

Общие слова

Профилирование приложений — это процесс анализа программы для определения её характеристик: времени выполнения различных частей кода и использования ресурсов.

Основные этапы профилирования всегда более-менее одинаковы:

  • Измерение времени выполнения. Cколько времени требуется для выполнения различных частей кода?
  • Анализ использования памяти. Сколько памяти потребляется различными частями программы?
  • Выявление узких мест. Какие части кода замедляют работу программы и/или используют слишком много ресурсов?
  • Оптимизация производительности. Принятие мер для улучшения скорости выполнения и эффективности использования ресурсов на основе полученных данных.

А как вообще работает профилировщик?

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

  • Детерминированные (deterministic) профилировщики. Главный представитель - встроенный cProfile. Такой профилировщик считает количество вызовов каждой функции и потраченное функцией время. Проблема в том, что время ожидания асинхронных вызовов не учитывается.
  • Статистические (statistical) профилировщики. Распространённые представители - scalene, py-spy, yappi, pyinstrument, austin. Такие профилировщики с некоторой частотой снимают "слепок" с процесса и применяют методы статистического анализа для поиска узких мест.

Основные типы узких мест в асинхронном Python-коде

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

Каждому типу сопоставим пример кода.

Список допущений

  • Python 3.12
  • Используется один и только один event-loop
  • Яндекс.Дзен не умеет отображать код текстом, поэтому пришлось вставлять его картинками.

Блокирующие операции

Async Python bottleneck #1

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

-2
Async Python bottleneck #2

Слишком частое переключение контекста

-3
Async Python bottleneck #3

Неравномерное распределение ресурсов

В англоязычной литературе такой сценарий называется "Resource Starvation".

-4
Async Python bottleneck #4

Чрезмерный расход памяти

-5
Async Python bottleneck #5

Использование "scalene" для профилирования

Почему scalene? Потому что этот инструмент позволяет профилировать и CPU, и GPU, и память; 10k+ звёзд на гитхабе, проект активно развивается.

Посмотрим что скажет scalene для каждого "проблемного" кода из списка выше.

Запускать будем в режиме scalene --cpu --memory --cli script_name.py

Блокирующие операции

-6

Проблемную строку с блокирующим вызовов видно сразу - 2% времени на Python, 98% - на системные вызовы.

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

-7

Здесь чуть сложнее. Видно, что 90% времени уходит на системные вызовы, но поменялась строка - теперь это сам asyncio.run(). Такой паттерн вывода профилировщика лучше всего просто запомнить.

Слишком частое переключение контекста

-8

Видим, как растёт потребление памяти в asyncio.gather() - делаем вывод о слишком сильном "дроблении" задач.

Неравномерное распределение ресурсов

-9

И снова соотношение времени system vs python не в пользу python-операций.

Чрезмерный расход памяти

-10

Здесь профилировщик сделал всё за нас и сразу показал проблемный код.

Заключение

Надо обратить внимание, что для трёх случаев - "блокирующие операции", "последовательный вызов асинхронных задач" и "неравномерное распределение ресурсов" профилировщик показал нам одну и ту же картину - system % >> python %. Для уточнения причины требуется, собственно, разработчик.

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