Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Профилирование Python-приложений: cProfile, память, узкие места

Профилирование Python на ноутбуке: находим узкие места по CPU и памяти за один вечер

Практическое профилирование Python-кода: cProfile и snakeviz, line_profiler и memory-profiler, типовые узкие места (циклы, I/O, pandas), интерпретация результатов и безопасные оптимизации.

Прямой ответ

Профилирование отвечает на вопрос «где именно медленно/прожорливо» измерениями, а не догадками. Минимальный набор инструментов встроен или ставится за минуту: cProfile (CPU по функциям, стандартная библиотека), snakeviz (визуализация отчёта), line_profiler (построчно внутри функции), memory_profiler (рост памяти по строкам). Правило практики: сначала cProfile → находим 2–3 функции, дающие 80% времени → внутри них line_profiler → оптимизируем только их. Оптимизация без профиля — это переписывание наугад.

Шаг 1. CPU-профиль целиком

python -m cProfile -o prof.out main.py
snakeviz prof.out        # интерактивная визуализация в браузере

Или из кода/ноутбука:

import cProfile, pstats
cProfile.run("main()", "prof.out")
p = pstats.Stats("prof.out"); p.sort_stats("cumulative").print_stats(20)

Как читать: tottime — время в самой функции (без вызовов), cumtime — с вызовами. Ищите функции с большим tottime и высоким ncalls (миллионы вызовов мелкой функции — классическое узкое место). Отдельный сигнал: огромное время в read/recv — это ожидание I/O, и лечить его нужно не кодом, а батчингом/кэшем.

Шаг 2. Построчный разбор горячей функции

pip install line_profiler
kernprof -l -v slow_module.py     # @profile на нужной функции

Типовые находки в Python-коде:

  • повторные вычисления в цикле (регулярные выражения, парсинг конфигурации) — выносятся наружу;
  • поиск в списке if x in big_list — замена на set даёт ускорение в сотни раз;
  • конкатенация строк в цикле — ''.join(parts) вместо +=;
  • частые мелкие обращения к атрибутам/индексам в горячем цикле — локальная переменная;
  • datetime.strptime/json.loads внутри цикла по миллионам строк — кэширование или векторизация.

Шаг 3. Память: где течёт и что пикует

from memory_profiler import profile
@profile
def process():
    ...

Запуск покажет прирост по каждой строке. Три типовых сценария:

  1. Пик в одной операции — обычно промежуточная копия данных (pandas merge/apply); лечится чанками и выбором колонок, приёмы — в статье про Jupyter и большие данные.
  2. Монотонный рост — коллекция-аккумулятор без очистки, кэш без ограничения (functools.lru_cache(maxsize=...) — с ограничением!), подписки на события без отписки.
  3. Рост в долгоживущем сервисе — снимки объектов через tracemalloc (стандартная библиотека):
import tracemalloc
tracemalloc.start()
# ... работа ...
for stat in tracemalloc.take_snapshot().statistics("lineno")[:10]:
    print(stat)

Интерпретация: что делать с результатами

  • Профилировщик добавляет накладные расходы (cProfile — до десятков процентов); относительные доли важнее абсолютных миллисекунд.
  • Профилируйте на реалистичных данных: на 100 строках узкое место может не проявиться.
  • После каждой оптимизации — повторный профиль: иногда «ускорение» съедается другим местом, а иногда оптимизированная функция давала лишь 3%.
  • Порядок рычагов обычно такой: алгоритм/структуры данных → батчинг и кэш → векторизация (numpy/polars) → параллелизм (concurrent.futures) → и только потом экзотика (Cython, компиляторы).

Профилирование в IDE и CI-гигиена

PyCharm/VS Code имеют встроенные профайлеры с тем же cProfile под капотом — удобно для быстрого цикла «правка → прогон»; бюджеты памяти IDE при этом учитывайте по статье про IDE и ОЗУ. Для команды полезно: регрессионные замеры времени критичного пути в CI (pytest-benchmark), чтобы замедление ловилось коммитом, а не жалобой пользователя. Окружение для замеров — фиксированное (lock-файл), иначе сравниваете версии библиотек, а не код; устройство окружений — в материале про venv/conda/uv.

Разбор типового кейса: «скрипт работает 40 минут»

Показательный сценарий, повторяющийся от проекта к проекту. Скрипт обработки CSV на 2 млн строк выполняется 40 минут; интуиция подсказывает «мало памяти, нужен сервер». Профиль показывает другое:

  1. cProfile → 62% tottime в pandas.core.frame.apply построчного вызова, 18% — в json.loads внутри цикла, 9% — чтение файла.
  2. line_profiler по функции-обработчику: apply(axis=1) вызывает Python-функцию 2 млн раз — это и есть узкое место, а не память.
  3. Правки: построчный apply заменён на векторизованные операции (str-аксессоры, np.where), парсинг JSON вынесен в один проход с кэшем по уникальным значениям, чтение — usecols + dtype.
  4. Результат: 40 минут → 3,5 минуты на том же ноутбуке, потребление памяти почти не изменилось.

Мораль кейса: до профилирования «тормоза» объясняли железом, после — алгоритмом. Апгрейд ОЗУ не ускорил бы этот скрипт ни на минуту. Обратная ситуация тоже встречается: профиль показывает честный CPU-bound в численных расчётах — тогда помогают векторизация/компиляция или действительно более мощный процессор.

Порядок принятия решения общий: сначала измерение, потом покупка. Если профиль показывает I/O-ожидание — смотрите на диск и сеть; если свопинг — на память; если CPU — на алгоритм. Методику измерения потребления памяти системой см. в статье про IDE и ОЗУ, инструменты построчного разбора памяти — выше по тексту.

FAQ

cProfile искажает результаты — чем заменить для точности? Семплирующие профайлеры (py-spy) не замедляют процесс и подходят для продакшн-диагностики: py-spy top --pid <pid>. Для офлайн-разбора cProfile точнее по числу вызовов.

Как профилировать асинхронный код? cProfile видит корутины как функции, но ожидание событий искажает картину; используйте специализированные инструменты (yappi с режимом async) и смотрите на wall-time этапов.

Стоит ли профилировать в Jupyter? Да: %prun (cProfile) и %memit работают прямо в ячейке — быстрый способ найти горячую точку до переноса пайплайна в скрипт.

Профиль показал, что 60% времени — json.loads. Что делать? Смотрите, откуда данные: повторяющийся парсинг одного и того же — кэшируйте; большие объёмы — переходите на более быстрые форматы (parquet, msgpack) или библиотеки-парсеры (orjson).

Как профилировать веб-сервис под нагрузкой, а не скрипт? Поднимите локальную нагрузку (hey/locust/k6) и снимайте семплы py-spy параллельно: py-spy record -o flame.svg --pid <pid>. Flamegraph покажет горячие пути под реальным конкурентным профилем — часто они отличаются от однопоточного прогона cProfile.

Профиль показывает много времени в блокировках БД. Это код или база? Смотрите долю: если recv/read сокета доминируют — запросы долгие, и начинать нужно с EXPLAIN и индексов на стороне БД, а не с Python. Код-сайд поможет кэш и батчинг запросов (N+1 — классика ORM).

Отдельная дисциплина — фиксация результатов: заведите в репозитории файл PERF.md (или тег в issue) с датой, версией кода, железом и ключевыми цифрами профиля. Через месяц оптимизаций только так можно показать «было 40 минут — стало 3,5», а не спорить по ощущениям. Для команды этот файл быстро становится источником правды о том, где узкое место системы на самом деле.

Источники: документация Python (profile/tracemalloc), line_profiler, memory_profiler (ссылки в шапке).