Общие слова
Профилирование приложений — это процесс анализа программы для определения её характеристик: времени выполнения различных частей кода и использования ресурсов.
Основные этапы профилирования всегда более-менее одинаковы:
-
Измерение времени выполнения. Cколько времени требуется для выполнения различных частей кода?
-
Анализ использования памяти. Сколько памяти потребляется различными частями программы?
-
Выявление узких мест. Какие части кода замедляют работу программы и/или используют слишком много ресурсов?
-
Оптимизация производительности. Принятие мер для улучшения скорости выполнения и эффективности использования ресурсов на основе полученных данных.
А как вообще работает профилировщик?
Детальному обзору будет посвящена отдельная статья, пока можно ограничится базовой классификацией:
-
Детерминированные (deterministic) профилировщики. Главный представитель — встроенный
cProfile. Такой профилировщик считает количество вызовов каждой функции и потраченное функцией время. Проблема в том, что время ожидания асинхронных вызовов не учитывается. -
Статистические (statistical) профилировщики. Распространённые представители —
scalene,py-spy,yappi,pyinstrument,austin. Такие профилировщики с некоторой частотой снимают «слепок» с процесса и применяют методы статистического анализа для поиска узких мест.
Основные типы узких мест в асинхронном Python-коде
Для асинхронного кода существует небольшое количество специфических «узких мест», которые лучше перечислить заранее.
Каждому типу сопоставим пример кода.
Список допущений
-
Используется один и только один event-loop
-
Python 3.12
Блокирующие операции
import asyncio import time async def main(): print('Start') # Blocking call time.sleep(3) # This blocks the entire event loop print('End') asyncio.run(main())
Последовательный вызов асинхронных задач
import asyncio import aiohttp async def fetch(session, url): async with session.get(url) as response: return await response.text() async def main(): urls = ["http://habr.com"] * 10 async with aiohttp.ClientSession() as session: # Inefficient: Sequential requests for url in urls: await fetch(session, url) asyncio.run(main())
Слишком частое переключение контекста
import asyncio async def tiny_task(): await asyncio.sleep(0.0001) async def main(): # Excessive context switching due to many small tasks await asyncio.gather(*(tiny_task() for _ in range(100000))) asyncio.run(main())
Неравномерное распределение ресурсов
В англоязычной литературе такой сценарий называется «Resource Starvation».
import asyncio async def long_running_task(): await asyncio.sleep(10) print("Long task executed") async def quick_task(): await asyncio.sleep(1) print("Quick task executed") async def main(): await asyncio.gather( long_running_task(), quick_task() # May be delayed excessively ) asyncio.run(main())
Чрезмерный расход памяти
import asyncio async def large_data_task(): data = "h" * 10**8 # Large memory usage await asyncio.sleep(1) async def main(): tasks = [large_data_task() for _ in range(100)] # High memory consumption await asyncio.gather(*tasks) asyncio.run(main())
Использование «scalene» для профилирования
Почему scalene? Потому что этот инструмент позволяет профилировать и CPU, и GPU, и память; 10k+ звёзд на гитхабе, проект активно развивается.
Посмотрим что скажет scalene для каждого «проблемного» кода из списка выше.
Запускать будем в режиме scalene --cpu --memory --cli script_name.py
Блокирующие операции

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

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

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

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

Здесь профилировщик сделал всё за нас и сразу показал проблемный код.
Заключение
Надо обратить внимание, что для трёх случаев — «блокирующие операции», «последовательный вызов асинхронных задач» и «неравномерное распределение ресурсов» профилировщик показал нам одну и ту же картину — system % >> python %. Для уточнения причины требуется, собственно, разработчик.
Профилировать Python — несложная и достаточно приятная задача, если знать основные типы узких мест и быть готовым внимательно читать вывод профилировщика.
ссылка на оригинал статьи https://habr.com/ru/articles/777232/
Добавить комментарий