Как стать автором
Обновить

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

Уровень сложностиПростой
Время на прочтение3 мин
Количество просмотров6.9K

Общие слова

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

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

  • Измерение времени выполнения. 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 - несложная и достаточно приятная задача, если знать основные типы узких мест и быть готовым внимательно читать вывод профилировщика.

Теги:
Хабы:
Всего голосов 13: ↑13 и ↓0+13
Комментарии3

Публикации

Истории

Работа

Data Scientist
59 вакансий
Python разработчик
116 вакансий

Ближайшие события

Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн
Антиконференция X5 Future Night
Дата30 мая
Время11:00 – 23:00
Место
Онлайн
Конференция «IT IS CONF 2024»
Дата20 июня
Время09:00 – 19:00
Место
Екатеринбург