Комикс про отладку и профилирование кода Python для увеличения скорости работы.

Думаете, ваш код на Python работает медленно? Перестаньте гадать и начните измерять скорость.

Практическое руководство по использованию cProfile + SnakeViz для поиска (и исправления) «горячих» участков кода.

Делиться

4264ffafa9af65a4dfe25087f389b99e

На днях я работал над скриптом для обработки данных, и это меня просто сводило с ума. Он работал, конечно, но просто… очень медленно. У меня было ощущение, что всё могло бы работать намного быстрее, если бы я смог понять, в чём задержка.

Первой мыслью было начать что-то подправлять. Можно было бы оптимизировать загрузку данных. Или переписать цикл for? Но я остановился. Я уже попадал в эту ловушку раньше, тратя часы на «оптимизацию» фрагмента кода, только чтобы обнаружить, что это почти не повлияло на общее время выполнения. Дональд Кнут был прав, когда сказал: «Преждевременная оптимизация — корень всех зол».

Я решил применить более методичный подход. Вместо того чтобы гадать, я собирался выяснить наверняка. Мне нужно было провести профилирование кода, чтобы получить точные данные о том, какие именно функции потребляют большую часть тактовых циклов.

В этой статье я подробно расскажу о процессе, который использовал я. Мы возьмем намеренно медленный скрипт на Python и воспользуемся двумя замечательными инструментами, чтобы с хирургической точностью определить его узкие места.

Первый из этих инструментов называется cProfile , это мощный профилировщик, встроенный в Python. Другой называется snakeviz, это… блестящий инструмент что Преобразует выходные данные профилировщика в интерактивную визуальную карту.

Настройка среды разработки

Прежде чем начать кодировать, давайте настроим нашу среду разработки. Рекомендуется создать отдельную среду Python, где вы сможете установить необходимое программное обеспечение и экспериментировать, зная, что ваши действия не повлияют на остальную часть системы. Я буду использовать для этого conda, но вы можете использовать любой знакомый вам метод.

# Создаем тестовую среду conda create -n profiling_lab python=3.11 -y # Теперь активируем ее conda activate profiling_lab

Теперь, когда мы настроили среду, нам нужно установить snakeviz для визуализации и numpy для примера скрипта. cProfile уже включен в Python, поэтому больше ничего делать не нужно. Поскольку мы будем запускать наши скрипты с помощью Jupyter Notebook, мы также установим его.

# Установите наш инструмент визуализации и numpy: pip install snakeviz numpy jupyter

Теперь введите jupyter notebook в командной строке. В браузере должно открыться окно Jupyter Notebook. Если этого не произойдет автоматически, после команды jupyter notebook вы, скорее всего, увидите экран с информацией. В нижней части экрана будет указан URL-адрес, который следует скопировать и вставить в браузер, чтобы запустить Jupyter Notebook.

Ваш URL будет отличаться от моего, но должен выглядеть примерно так:

http://127.0.0.1:8888/tree?token=3b9f7bd07b6966b41b68e2350721b2d0b6f388d248cc69da

Подготовив необходимые инструменты, пришло время взглянуть на код, который мы собираемся исправить.

Наш сценарий «проблемы»

Для корректного тестирования наших инструментов профилирования нам нужен скрипт, демонстрирующий явные проблемы с производительностью. Я написал простую программу, которая имитирует проблемы обработки данных с использованием памяти, итераций и циклов ЦП, что делает её идеальным кандидатом для нашего исследования.

# run_all_systems.py import time import math # =================================================================== CPU_ITERATIONS = 34552942 STRING_ITERATIONS = 46658100 LOOP_ITERATIONS = 171796964 # ==================================================================== # — Задача 1: Откалиброванное узкое место, ограниченное ЦП — def cpu_heavy_task(iterations): print(» -> Выполняется с ограничением ЦП задача…») результат = 0 для i в диапазоне (итерации): результат += math.sin(i) * math.cos(i) + math.sqrt(i) вернуть результат # — Задача 2: Откалиброванное узкое место памяти/строки — def memory_heavy_string_task(iterations): print(» -> Выполняется задача, ограниченная памятью/строкой…») отчет = «» chunk = «report_item_abcdefg_123456789_» для i в диапазоне (итерации): report += f»|{chunk}{i}» вернуть отчет # — Задача 3: Откалиброванное узкое место итерации «Тысяча разрезов» — def simulate_tiny_op(n): pass def iteration_heavy_task(iterations): print(» -> Выполняется задача, ограниченная итерацией…») для i в диапазоне (итерации): simulate_tiny_op(i) вернуть «OK» # — Главный оркестратор — def run_all_systems(): print(«— Начало финального медленного сбалансированного тестирования —«) cpu_result = cpu_heavy_task(iterations=CPU_ITERATIONS) string_result = memory_heavy_string_task(iterations=STRING_ITERATIONS) iteration_result = iteration_heavy_task(iterations=LOOP_ITERATIONS) print(«— Финальное медленное сбалансированное тестирование завершено —«)

Шаг 1: Сбор данных с помощью cProfile

Наш первый инструмент, cProfile, — это детерминированный профилировщик, встроенный в Python. Мы можем запускать его из кода для выполнения нашего скрипта и записи подробной статистики о каждом вызове функции.

import cProfile, pstats, io pr = cProfile.Profile() pr.enable() # Запустить функцию, которую вы хотите профилировать run_all_systems() pr.disable() # Вывести статистику в строку и распечатать 10 лучших по суммарному времени s = io.StringIO() ps = pstats.Stats(pr, stream=s).sort_stats(«cumtime») ps.print_stats(10) print(s.getvalue())

Вот результат.

— Запуск финального медленного сбалансированного примера — -> Выполнение задачи, зависящей от ЦП… -> Выполнение задачи, зависящей от памяти/строки… -> Выполнение задачи, зависящей от итерации… — Финальный медленный сбалансированный пример завершен — 275455984 вызовов функций за 30,497 секунд. Отсортировано по: суммарному времени. Список сокращен с 47 до 10 из-за ограничения <10> ncalls tottime percall cumtime percall filename:lineno(function) 2 0.000 0.000 30.520 15.260 /home/tom/.local/lib/python3.10/site-packages/IPython/core/interactiveshell.py:3541(run_code) 2 0.000 0.000 30.520 15.260 {built-in method builtins.exec} 1 0.000 0.000 30.497 30.497 /tmp/ipykernel_173802/1743829582.py:41(run_all_systems) 1 9.652 9.652 14.394 14.394 /tmp/ipykernel_173802/1743829582.py:34(iteration_heavy_task) 1 7.232 7.232 12.211 12.211 /tmp/ipykernel_173802/1743829582.py:14(cpu_heavy_task) 171796964 4.742 0.000 4.742 0.000 /tmp/ipykernel_173802/1743829582.py:31(simulate_tiny_op) 1 3.891 3.891 3.892 3.892 /tmp/ipykernel_173802/1743829582.py:22(memory_heavy_string_task) 34552942 1.888 0.000 1.888 0.000 {встроенный метод math.sin} 34552942 1.820 0.000 1.820 0.000 {встроенный метод math.cos} 34552942 1.271 0.000 1.271 0.000 {встроенный метод math.sqrt}

У нас есть множество чисел, которые могут быть сложными для интерпретации. Вот тут-то и пригодится SnakeViz.

Шаг 2: Визуализация узкого места с помощью SnakeViz.

Именно здесь происходит волшебство. Snakeviz берет результаты нашего файла профилирования и преобразует их в интерактивную диаграмму, отображаемую в браузере, что упрощает поиск узких мест.

Давайте воспользуемся этим инструментом, чтобы визуализировать то, что у нас есть. Поскольку я использую Jupyter Notebook, нам нужно сначала его загрузить.

%load_ext snakeviz

И мы работаем вот так.

%%snakeviz main()

Результат состоит из двух частей. Первая — это визуализация, подобная этой.

81889f39a427013858bc7b707632b165

Перед вами диаграмма в виде «сосульки», построенная сверху вниз. Сверху вниз она отображает иерархию звонков.

В самом верху: Python выполняет наш скрипт (<встроенный метод builtins exec>).

Далее: выполнение скрипта __main__ (:1()). Затем функция run_all_systems. Внутри неё вызываются две ключевые функции: iteration_heavy_task и cpu_heavy_task.

Часть, требующая больших объемов памяти для обработки данных, не обозначена на диаграмме. Это связано с тем, что доля времени, затрачиваемого на эту задачу, значительно меньше, чем время, отведенное на две другие ресурсоемкие функции. В результате справа от блока cpu_heavy_task мы видим гораздо меньший, немаркированный блок.

Обратите внимание, что для анализа существует также стиль диаграммы Snakeviz, называемый диаграммой «Солнечный луч». Она немного похожа на круговую диаграмму, за исключением того, что содержит набор концентрических кругов и дуг всё большего размера . Идея заключается в том, что время выполнения функций представляется угловым размером дуги круга. Корневая функция — это круг в центре визуализации. Корневая функция выполняется путем вызова подфункций под ней и так далее. В этой статье мы не будем рассматривать этот тип отображения.

Визуальное подтверждение, подобное этому, может быть гораздо более эффективным, чем простое разглядывание таблицы цифр. Мне больше не нужно было гадать, куда смотреть; данные были прямо передо мной.

За визуализацией быстро следует текстовый блок с подробным описанием времени выполнения различных частей вашего кода, аналогично выводу инструмента cprofile. Я показываю только первые около дюжины строк, так как всего их было более 30.

ncalls tottime percall cumtime percall filename:lineno(function) —————————————————————- 1 9.581 9.581 14.3 14.3 1062495604.py:34(iteration_heavy_task) 1 7.868 7.868 12.92 12.92 1062495604.py:14(cpu_heavy_task) 171796964 4.717 2.745e-08 4.717 2.745e-08 1062495604.py:31(simulate_tiny_op) 1 3.848 3.848 3.848 3.848 1062495604.py:22(memory_heavy_string_task) 34552942 1.91 5.527e-08 1.91 5.527e-08 ~:0(<встроенный метод math.sin>) 34552942 1.836 5.313e-08 1.836 5.313e-08 ~:0(<встроенный метод math.cos>) 34552942 1.305 3.778e-08 1.305 3.778e-08 ~:0(<встроенный метод math.sqrt>) 1 0.02127 0.02127 31.09 31.09 :1() 4 0.0001764 4.409e-05 0.0001764 4.409e-05 socket.py:626(send) 10 0.000123 1.23e-05 0.0004568 4.568e-05 iostream.py:655(write) 4 4.594e-05 1.148e-05 0.0002735 6.838e-05 iostream.py:259(schedule) … … …

Шаг 3: Решение проблемы

Конечно, такие инструменты, как cProfiler и Snakeviz, не подскажут, как решить проблемы с производительностью, но теперь, когда я точно знал, где находятся проблемы, я мог применять целенаправленные исправления.

# final_showcase_fixed_v2.py import time import math import numpy as np # ================================================================== CPU_ITERATIONS = 34552942 STRING_ITERATIONS = 46658100 LOOP_ITERATIONS = 171796964 # =================================================================== # — Исправление 1: Векторизация для задач, интенсивно использующих ЦП — def cpu_heavy_task_fixed(iterations): «»» Исправлено с помощью NumPy для выполнения сложных математических вычислений над всем массивом сразу в высокооптимизированном коде на C вместо цикла Python. «»» print(» -> Выполняется задача, сильно зависящая от ЦП…») # Создание массива чисел от 0 до iterations-1 i = np.arange(iterations, dtype=np.float64) # Тот же расчет, но векторизованный, на порядки быстрее result_array = np.sin(i) * np.cos(i) + np.sqrt(i) return np.sum(result_array) # — Исправление 2: Эффективное объединение строк — def memory_heavy_string_task_fixed(iterations): «»» Исправлено с помощью генератора списков и одного эффективного вызова ''.join(). Это позволяет избежать создания миллионов промежуточных строковых объектов. «»» print(» -> Выполняется «Задача, ограниченная памятью/строкой…») chunk = «report_item_abcdefg_123456789_» # Списковое выражение быстрое и эффективно использует память parts = [f»|{chunk}{i}» for i in range(iterations)] return «».join(parts) # — Исправление 3: Устранение цикла «Тысяча разрезов» — def iteration_heavy_task_fixed(iterations): «»» Исправлено путем распознавания того, что задача может быть пустой операцией или пакетной операцией. В реальном сценарии вы бы нашли способ полностью избежать цикла. Здесь мы демонстрируем исправление, просто удалив бессмысленный цикл. Цель состоит в том, чтобы показать, что проблема заключалась в стоимости самого цикла. «»» print(» -> Выполнение задачи, ограниченной итерацией…») # Исправление состоит в том, чтобы найти пакетную операцию или исключить необходимость в цикле. # Поскольку исходная функция ничего не делала, исправление состоит в том, чтобы ничего не делать, но быстрее. return «OK» # — Главный оркестратор — def run_all_systems(): «»» Главный оркестратор теперь вызывает FAST-версии задач. «»» print(«— Запуск финальной FAST-версии сбалансированной задачи —«) cpu_result = cpu_heavy_task_fixed(iterations=CPU_ITERATIONS) string_result = memory_heavy_string_task_fixed(iterations=STRING_ITERATIONS) iteration_result = iteration_heavy_task_fixed(iterations=LOOP_ITERATIONS) print(«— Финальная FAST-версия сбалансированной задачи завершена —«)

Теперь мы можем повторно запустить cProfiler на обновленном коде.

import cProfile, pstats, io pr = cProfile.Profile() pr.enable() # Запустить функцию, которую вы хотите профилировать run_all_systems() pr.disable() # Вывести статистику в строку и распечатать 10 лучших по суммарному времени s = io.StringIO() ps = pstats.Stats(pr, stream=s).sort_stats(«cumtime») ps.print_stats(10) print(s.getvalue()) # # начало вывода # — Запуск финального сбалансированного теста FAST — -> Выполнение задачи, зависящей от ЦП… -> Выполнение задачи, зависящей от памяти/строки… -> Выполнение задачи, зависящей от итераций… — Финальный сбалансированный тест FAST завершен — 197 вызовов функций за 6,063 секунды. Отсортировано по: суммарному времени. Список сокращен с 52 до 10 из-за ограничения <10> ncalls tottime percall cumtime percall filename:lineno(function) 2 0.000 0.000 6.063 3.031 /home/tom/.local/lib/python3.10/site-packages/IPython/core/interactiveshell.py:3541(run_code) 2 0.000 0.000 6.063 3.031 {built-in method builtins.exec} 1 0.002 0.002 6.063 6.063 /tmp/ipykernel_173802/1803406806.py:1() 1 0.402 0.402 6.061 6.061 /tmp/ipykernel_173802/3782967348.py:52(run_all_systems) 1 0.000 0.000 5.152 5.152 /tmp/ipykernel_173802/3782967348.py:27(memory_heavy_string_task_fixed) 1 4.135 4.135 4.135 4.135 /tmp/ipykernel_173802/3782967348.py:35() 1 1.017 1.017 1.017 1.017 {метод 'join' объектов 'str'} 1 0.446 0.446 0.505 0.505 /tmp/ipykernel_173802/3782967348.py:14(cpu_heavy_task_fixed) 1 0.045 0.045 0.045 0.045 {встроенный метод numpy.arange} 1 0.000 0.000 0.014 0.014 <__array_function__ internals>:177(sum)

Это фантастический результат, демонстрирующий возможности профилирования. Мы сосредоточили свои усилия на тех частях кода, которые имели значение. Для большей точности я также запустил snakeviz на исправленном скрипте.

%%snakeviz run_all_systems()

117800efd7c080168ddfce49fae4f106

Наиболее заметное изменение — сокращение общего времени выполнения с примерно 30 секунд до примерно 6 секунд. Это пятикратное ускорение, достигнутое за счет устранения трех основных узких мест, которые были видны в исходном профиле.

Давайте рассмотрим каждый из них по отдельности.

1. Задача с большим количеством итераций

До (Проблемы)
На первом изображении большая полоса слева, iteration_heavy_task, является самым большим узким местом, потребляющим 14,3 секунды .

  • Почему это работало медленно? Эта задача представляла собой классическую «смерть от тысячи порезов». Функция simulate_tiny_op практически ничего не делала, но вызывалась миллионы раз внутри чистого цикла for на Python. Огромные накладные расходы, связанные с многократным запуском и остановкой вызова функции интерпретатором Python, и были всей причиной замедления работы.

Решение
В исправленной версии, iteration_heavy_task_fixed, было учтено, что цель может быть достигнута без цикла. В нашем примере это означало полное удаление бессмысленного цикла. В реальном приложении это потребовало бы поиска одной «массовой» операции для замены итеративной.

После (результата)
На втором изображении полоса iteration_heavy_task полностью исчезла . Теперь скорость выполнения задачи настолько высока, что время её выполнения составляет ничтожно малую долю секунды, и она невидима на графике. Мы успешно устранили проблему, занимавшую 14,3 секунды.

2. Задача cpu_heavy_task

До (Проблемы)
Вторым основным узким местом, отчетливо видимым по большой оранжевой полосе справа, является задача cpu_heavy_task, выполнение которой заняло 12,9 секунды .

  • Почему это работало медленно? Как и в случае с итерацией, эта функция также была ограничена скоростью цикла for в Python. Хотя математические операции внутри были быстрыми, интерпретатору приходилось обрабатывать каждое из миллионов вычислений по отдельности, что крайне неэффективно для численных задач.

Решение
Решение заключалось в векторизации с использованием библиотеки NumPy. Вместо цикла Python, cpu_heavy_task_fixed создавал массив NumPy и одновременно выполнял все математические операции (np.sqrt, np.sin и т. д.) над всем массивом. Эти операции выполняются в высокооптимизированном, предварительно скомпилированном коде C, полностью обходя медленный цикл интерпретатора Python.

После (результата).
Как и в случае с первым узким местом, индикатор cpu_heavy_task исчез с диаграммы «после». Время его выполнения сократилось с 12,9 секунд до нескольких миллисекунд.

3. Задача обработки строк с большим объемом памяти

До возникновения проблемы:
На первой диаграмме выполнялась ресурсоемкая задача обработки строк, но время ее выполнения было небольшим по сравнению с двумя другими, более серьезными проблемами, поэтому она была отнесена к небольшому, не обозначенному фрагменту пространства в крайнем правом углу. Это была относительно незначительная проблема.

Решение
Решение этой задачи заключалось в замене неэффективного метода конкатенации строк report += “… ” на гораздо более эффективный способ: создание списка всех частей строки и последующий вызов метода “”.join() один раз в конце.

После (результата)
На второй диаграмме мы видим результат нашего успеха. Устранив два узких места, занимавших более 10 секунд, теперь доминирующим узким местом стала задача обработки строк, требующая больших объемов памяти, на которую приходится 4,34 секунды из общего времени выполнения в 5,22 секунды.

Snakeviz даже позволяет нам заглянуть внутрь этой исправленной функции. Новым наиболее значимым фактором является оранжевая полоса с меткой (списковое выражение), которая занимает 3,52 секунды . Это указывает на то, что даже в исправленном коде наиболее трудоемкой частью теперь является процесс создания обширного списка строк в памяти перед их объединением.

Краткое содержание

В этой статье представлено практическое руководство по выявлению и устранению проблем с производительностью в коде Python, и утверждается, что разработчикам следует использовать инструменты профилирования для измерения производительности, а не полагаться на интуицию или догадки при определении источника замедления.

Я продемонстрировал методичный рабочий процесс с использованием двух ключевых инструментов:

  • cProfile : Встроенный в Python профилировщик, используемый для сбора подробных данных о вызовах функций и времени их выполнения.
  • snakeviz : Инструмент визуализации, который преобразует данные cProfile в интерактивную диаграмму в виде «сосульки», позволяя легко визуально определить, какие части кода потребляют больше всего времени.

В статье на примере используется скрипт, намеренно замедляющий работу системы, в котором были выявлены три существенных и значительных «узких места»:

  1. Задача, ограниченная количеством итераций: функция, вызываемая миллионы раз в цикле, демонстрирующая снижение производительности из-за накладных расходов на вызов функций в Python («смерть от тысячи порезов»).
  2. Задача, сильно нагружающая процессор: цикл for, выполняющий миллионы математических вычислений, демонстрирующий неэффективность чистого Python для сложных численных задач.
  3. Задача, требующая большого объема памяти: построение большой строки неэффективным способом с помощью многократного объединения операторов +=.

Проанализировав результаты работы Snakeviz, я выявил эти три проблемы и применил целенаправленные меры по их устранению.

  • Проблема «узкого места» итераций была решена путем Устранение ненужного цикла.
  • Проблема узкого места в работе процессора была решена с помощью векторизации с использованием NumPy, которая выполняет математические операции в быстром скомпилированном коде на языке C.
  • Проблема нехватки памяти была решена путем добавления частей строки в список и использования одного эффективного вызова метода .join() .

Эти исправления привели к значительному ускорению работы, сократив время выполнения скрипта с более чем 30 секунд до чуть более 6 секунд . В заключение я продемонстрировал, что даже после устранения основных проблем профилировщик можно использовать снова для выявления новых, меньших узких мест, показав, что настройка производительности — это итеративный процесс, основанный на измерениях.

Источник: towardsdatascience.com

✅ Найденные теги: Python, Думаете,, Измерение, Код, новости, Скорость

ОСТАВЬТЕ СВОЙ КОММЕНТАРИЙ

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Каталог бесплатных опенсорс-решений, которые можно развернуть локально и забыть о подписках

галерея

Фото сгенерированных лиц: исследование показывает, что люди не могут отличить настоящие лица от сгенерированных
Нейросети построили капитализм за трое суток: 100 агентов Claude заперли…
Скетч: цифровой осьминог и виртуальный мир внутри компьютера с человечком.
Сцена с жестами пальцами, где один жест символизирует "VPN", а другой "KHP".
‼️Paramount купила Warner Bros. Discovery — сумма сделки составила безумные…
Скриншот репозитория GitHub "Claude Scientific Skills" AI для научных исследований.
Структура эффективного запроса Claude с элементами задачи, контекста и референса.
Эскиз и готовая веб-страница платформы для AI-дизайна в современном темном режиме.
ideipro logotyp
Image Not Found
Звёздное небо с галактиками и туманностями, космос, Вселенная, астрофотография.

Система оповещения обсерватории Рубина отправила 800 000 сигналов в первую ночь наблюдений.

Астрономы будут получать оповещения о небесных явлениях в течение нескольких минут после их обнаружения. Теренс О'Брайен, редактор раздела «Выходные». Публикации этого автора будут добавляться в вашу ежедневную рассылку по электронной почте и в ленту новостей на главной…

Мар 2, 2026
Женщина с длинными тёмными волосами в синем свете, нейтральный фон.

Расследование в отношении 61-фунтовой машины, которая «пожирает» пластик и выплевывает кирпичи.

Обзор компактного пресса для мягкого пластика Clear Drop — и что будет дальше. Шон Холлистер, старший редактор Публикации этого автора будут добавляться в вашу ежедневную рассылку по электронной почте и в ленту новостей на главной странице вашего…

Мар 2, 2026
Черный углеродное волокно с текстурой плетения, отражающий свет.

Материал будущего: как работает «бессмертный» композит

Учёные из Университета штата Северная Каролина представили композит нового поколения, способный самостоятельно восстанавливаться после серьёзных повреждений.  Речь идёт о модифицированном армированном волокном полимере (FRP), который не просто сохраняет прочность при малом весе, но и способен «залечивать» внутренние…

Мар 2, 2026
Круглый экран с изображением замка и горы, рядом электронная плата.

Круглый дисплей Waveshare для креативных проектов

Круглый 7-дюймовый сенсорный дисплей от Waveshare создан для разработчиков и дизайнеров, которым нужен нестандартный экран.  Это IPS-панель с разрешением 1 080×1 080 пикселей, поддержкой 10-точечного ёмкостного сенсора, оптической склейкой и защитным закалённым стеклом, выполненная в круглом форм-факторе.…

Мар 2, 2026

Впишите свой почтовый адрес и мы будем присылать вам на почту самые свежие новости в числе самых первых