π ΠΠ°ΠΊ ΡΡΠΊΠΎΡΠΈΡΡ ΠΊΠΎΠ΄ Π½Π° Python: Π²ΡΠ±ΠΈΡΠ°Π΅ΠΌ ΠΈΠ½ΡΡΡΡΠΌΠ΅Π½Ρ Π΄Π»Ρ ΠΎΠΏΡΠΈΠΌΠΈΠ·Π°ΡΠΈΠΈ ΠΏΡΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡΠ΅Π»ΡΠ½ΠΎΡΡΠΈ ΠΏΡΠΎΠ³ΡΠ°ΠΌΠΌΡ
В этой статье мы рассмотрим различные профилировщики (cProfile, Pyinstrument, Eliot) и обсудим их плюсы и минусы, а также когда и где их лучше применить. Данная статья является переводом. Ссылка на оригинал. Ваша Python-программа слишком медленная. Возможно, ваше веб-приложение не поспевает за ним, или некоторые запросы выполняются слишком долго. Возможно, у вас есть командный файл, выполнение которого занимает часы или даже дни. Что вы сделаете для ускорения работы программы? Вы, вероятно, сделаете следующее: 1. Выберите подходящий инструмент (программное средство) для измерения скорости. 2. Используете инструмент для поиска «узкого места». 3. Устраните «узкое место». В этой статье основное внимание будет уделено первому шагу: выбору правильного инструмента. И, в частности, он будет включать в себя: Я не буду вдаваться в подробности о том, как использовать эти инструменты, потому что задача состоит в том, чтобы помочь вам выбрать правильный. Но я объясню, что делают эти инструменты, а также когда и почему нужно выбрать тот или иной. Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека питониста» Интересно, перейти к каналу Профилировщик cProfile встроен в Python, поэтому вы, вероятно, слышали о нем, и, возможно, это тот инструмент, который вы используете по умолчанию. Он работает, отслеживая каждый вызов функции в программе. Вот почему это детерминированный профилировщик: если вы запустите его с теми же входными данными, он даст тот же результат. По умолчанию cProfile отсчитывает системное время — сколько времени прошло за период работы функции. Это означает, что он может измерять одновременно и скорость работы процессора, и другие проблемы, такие как спящий режим или конфликты при блокировках. Вы всегда должны задаваться вопросом что измеряет ваш профилировщик, потому что разные измерения могут обнаруживать разные проблемы. Хотя cProfile доступен по умолчанию в Python, у него также есть некоторые недостатки — и, как вы увидите чуть позже, вы вряд ли захотите его использовать для большинства случаев. Использовать cProfile довольно просто. Если у вас есть скрипт, то, скорее всего, вы запустите его так: 1 Затем вы можете просто добавить префикс python 2 Существует также профилирование для Python API, поэтому вы можете профилировать определенные функции в запросе интерпретатора Python или в Jupyter notebook. Выходной формат представляет собой таблицу, что не совсем удобно: каждая строка является результатом вызова функции, которая выполнялась в течение профилируемого промежутка времени, однако вы не знаете, как этот вызов связан с вызовами других функций. Поэтому, если у вас есть функция, которую можно вызвать с помощью нескольких путей выполнения кода, могут возникнуть сложности с определением, какой именно блок кода отвечает за вызов медленной функции. Если вы посмотрите на таблицу выше, то увидите, что: Используя эту информацию, вы можете определить медленные функции, которые можно оптимизировать. cProfile проводит измерение для каждого вызова функции. В частности, вызов функции во время ее выполнения выглядит следующим образом: Профилировщик записывает время в начале и в конце. Полученная разница является временем работы функции. Хотя cProfile всегда доступен при любой установке Python, он также имеет ряд существенных проблем. Как вы понимаете, выполнение дополнительной работы для каждого вызова функции требует определенных затрат: 4 Обратите внимание, насколько медленно работает cProfile. И что еще хуже, замедление происходит неравномерно для всей программы: поскольку оно связано с количеством вызовов функций, блоки вашего кода с большим количеством таких вызовов будут замедляться еще больше. Таким образом, эти накладные расходы могут исказить результат. Если вы помните, выходные данные cProfile, которые мы видели выше, содержат строку для каждой отдельной функции, которая была вызвана во время выполнения программы. Большинство данных вызовов функций не имеют отношения к нашей проблеме с производительностью: они выполняются быстро и только один или два раза. Поэтому, когда вы читаете выходные данные cProfile, вы имеете дело с большим количеством шума, который мешает распознать сигнал. Довольно часто ваша программа будет работать медленно только в реальных условиях с реальными входными данными. Возможно, только определенные запросы пользователей замедляют работу вашего веб-приложения, и вы не знаете какие именно. Возможно, ваш командный файл работает медленно только с реальными данными. Но cProfile, как мы видели, немного замедляет вашу программу, и поэтому вы, вероятно, не захотите запускать его в своем рабочем проекте. Таким образом, в то время как низкая скорость работы появляется только при работе с реальными проектами, cProfile может быть полезен только на стадии разработки. cProfile может сообщить вам о том, что « Но иногда у вас есть алгоритмический код, который работает медленно только при определенных входных данных. Вполне возможно, что: cProfile не сможет сказать вам, какие входные данные вызвали замедление работы, что может затруднить диагностику проблемы. Обобщая все недостатки, cProfile не должен быть вашим предпочтительным инструментом для повышения производительности. Поэтому далее мы поговорим о его двух альтернативах: Pyinstrument решает две проблемы, которые мы рассмотрели выше, а именно: Pyinstrument измеряет системное время, а не процессорное, поэтому он также может фиксировать замедление, вызванное сетевыми запросами, записью на диск, конфликтом блокировок и т. д. Использование Pyinstrument аналогично cProfile — просто добавьте префикс к вашему скрипту: 7 Обратите внимание, что у него есть некоторые накладные расходы, но не такие большие, как у cProfile, при этом затраты вычислительных ресурсов распределены равномерно. Pyinstrument также поддерживает API Python, так что вы можете использовать его для профилирования определенных фрагментов кода в интерактивном интерпретаторе Python или блокноте Jupyter. На выходе Pyinstrument представляет собой дерево вызовов, измеряющее системное время: 8 В отличие от построчной функции cProfile, Pyinstrument дает вам дерево вызовов функций, поэтому вы можете получить информацию о скорости работы. Функция может появляться несколько раз, если низкая скорость работы вызвана несколькими путями выполнения кода. В результате вывод Pyinstrument намного проще интерпретировать, ведь он предоставляет более подробное понимание структуры производительности вашей программы, чем вывод cProfile по умолчанию. Представьте, что у вас есть кошка. Вы хотите знать, как эта кошка проводит свое время. Вы могли бы непрерывно шпионить за ней, но это был бы весьма трудоемкий процесс. Поэтому вместо этого вы решаете периодически делать «замеры»: каждые 5 минут вы заглядываете в комнату, где находится кошка, и записываете, что она делает. Например: Через несколько дней можно подвести итоги своих наблюдений: Итак, насколько точно это наблюдение? Поскольку ваша цель — изучить, где кошка проводит большую часть своего времени, информационная сводка, вероятно, будет точной. И чем чаще проводятся наблюдения (==измерения) и чем больше наблюдений вы сделаете, тем точнее будет сводка. Если ваша кошка проводит большую часть своего времени во сне, вы ожидаете, что большинство выборочных наблюдений покажут, что она спит. И да, вы пропустите некоторые быстрые и редкие действия, но для целей «на что кошка тратила большую часть своего времени» эти быстрые и редкие действия не имеют значения. Как и наш кот, Pyinstrument периодически наблюдает за поведением Python-программы: каждую 1 мс он проверяет, какая функция запущена в данный момент. Это означает: Это означает, что в нашей сводке производительности меньше шума: функции, которые почти не используются, в основном будут отсутствовать. Но в целом сводка довольно точна с точки зрения того, как программа потратила свое время, если мы произвели достаточное количество записей наблюдений. Последним инструментом, который мы подробно рассмотрим, является Eliot, написанная мной библиотека логирования. Она решает оставшиеся две проблемы, которые мы видели в cProfile: Как вы видите, Eliot предоставляет некоторые уникальные возможности, которые делают лучше его запись производительности, чем у обычных библиотек логирования. При этом, проделав некоторую дополнительную работу, вы можете получить те же преимущества и от других библиотек ведения логов. Рассмотрим следующий набросок программы: 8 Мы можем взять этот код и добавить к нему логирование: В частности, мы делаем две вещи: У Eliot есть другие, более детализированные API, но Как только мы запустим программу, мы сможем просмотреть журналы с помощью инструмента под названием eliot-tree: 9 Обратите внимание на сходство с Pyinstrument – дерево действий. Я немного упростил вывод — изначально так, чтобы он поместился на слайде, который я использовал в статье для доклада, — но даже в этой версии этого достаточно, чтобы оценить производительность. У Eliot каждое действие имеет начало и конец и может запускать другие действия, в результате чего получается дерево. Поскольку мы знаем, когда начинается и заканчивается каждое зарегистрированное действие, мы также знаем, сколько времени оно заняло. В этом случае каждое действие сопоставляется с вызовом функции в отношении один к одному. И есть некоторые отличия от вывода Pyinstrument: Например, вы можете видеть, что Логирование само по себе не является достаточным источником информации о производительности. Во-первых, вы получаете информацию только из кода, в который вы явно добавили вызовы ведения журнала. Профилировщик может работать с любым произвольным фрагментом кода без предварительной подготовки, но с логированием вам придется проделать некоторую работу заранее. Если вы не добавили код ведения журнала, вы не получите никакой информации. Eliot делает это немного лучше, поскольку структура дерева действий дает вам некоторое представление о том, где было потрачено время, но этого все же недостаточно, если логирование слишком редкое. Во-вторых, вы не можете добавлять логирование везде, потому что это замедлит вашу программу. Ведение журнала влечет за собой накладные расходы — это требует больше затрат, чем у cProfile. Поэтому вам нужно добавлять его разумно, в ключевые моменты, где он будет максимизировать информацию, которую он дает, не влияя на производительность. Итак, когда вы должны использовать каждый инструмент? Любая нетривиальная программа, вероятно, нуждается в некотором логировании, хотя бы для того, чтобы отлавливать баги и ошибки. И если вы уже добавляете ведение журнала, вы также можете позаботиться о том, чтобы регистрировать информацию, необходимую для отладки производительности. Eliot делает это проще, поскольку регистрация действий по своей сути предоставляет вам информацию о затраченном время. Однако, приложив усилия, вы можете сделать это для любой библиотеки логирования. Ведение журнала может помочь вам определить конкретное место, где ваша программа работает медленно, и, по крайней мере, некоторые входные данные, вызывающие замедления, но этого часто недостаточно. Итак, следующим шагом будет использование профилировщика, в частности, профилировщика выборки, такого как Pyinstrument: Если вам когда-нибудь понадобится написать собственный профилировщик, cProfile позволит вам подключить различные функции затрат, что делает его простым инструментом для измерения более необычных показателей. Вы можете измерить: В качестве хорошей отправной точки для работы с инструментами оптимизации производительности я предлагаю вам следующий алгоритм: *** Из этой статьи мы узнали:1. cProfile: Детерминированный профайлер
Использование cProfile
$ python benchmark.py 7855 messages/sec
-m cProfile
и запустить его с профилировщиком:
$ python -m cProfile benchmark.py 1444992 function calls (1443814 primitive calls) in 1.208 seconds Ordered by: cumulative time ncalls tottime percall cumtime percall filename:lineno(function) 1 0.000 0.000 1.208 1.208 benchmark.py:7(<module>) 1 0.048 0.048 1.151 1.151 benchmark.py:21(run) 50000 0.031 0.000 0.657 0.000 _output.py:182(write) 50000 0.045 0.000 0.618 0.000 _output.py:73(send) 50000 0.064 0.000 0.566 0.000 _output.py:452(__call__) ... a million more lines of output ...
Что cProfile может вам сказать
_output.py(__call__)
вызвали 50 000 раз. У нас получилось четное число, потому что это тестовый скрипт, который запускает один и тот же код в цикле 10 000 раз. Если вы не вызывали функцию намеренно множество раз, то это может быть полезно для определения большого количества вызовов для выявления занятых внутренних циклов._output.py(send)
работал в течение 0.618 секунд (включая время, затраченное на вызываемые им функции) и 0.045 секунд (не считая вызываемых им функций).Как это работает
start = time() try: f() finally: elapsed = time() - start
Проблемы с cProfile
Проблема №1: Высокие затраты вычислительных ресурсов и искаженные результаты
$ python benchmark.py 7855 messages/sec $ python -m cProfile benchmark.py 5264 messages/sec ... cProfile output ...
Проблема №2: Слишком много информации
Проблема № 3: Измерение результатов работы на локальном компьютере
Проблема № 4: Производительность измеряется только для функций
slowfunc()
работает медленно», когда он усредняет все входные данные для этой функции. И это нормально, если функция сама по себе всегда медленная.slowfunc(100)
выполняется быстро.slowfunc(0)
выполняется медленно.cProfile: Его, как правило, недостаточно
2. Pyinstrument: Профилировщик выборки
Как вы можете его использовать
$ python benchmark.py 7561 messages/sec $ python -m pyinstrument benchmark.py 6760 messages/sec ... pyinstrument output ...
Выходные данные
1.506 <module> benchmark.py:7 ├─ 1.477 run benchmark.py:21 │ ├─ 0.604 attach myapp/_code.py:52 │ │ └─ 0.598 write myapp/_code.py:130 │ │ ├─ 0.557 final_write myapp/_foo.py:181 ...
Как это работает (кошачья версия)
Как это работает (программная версия)
3. Eliot: Библиотека логирования
Добавление логирования в существующий код
def add(a, b): # ... implementation ... def multiply(a, b): # ... implementation ... def multiplysum(a, b, c): return multiply(add(a, b), c) print(multiplysum(1, 2, 4))
from eliot import log_call, to_file to_file(open("out.log", "w")) @log_call def add(a, b): # ... implementation ... @log_call def multiply(a, b): # ... implementation ... # etc. Specifically, we do two
out.log
).@log_call
. Это позволяет зарегистрировать факт вызова функции, ее аргументы и возвращаемое значение (или возникшее исключение).@log_call
достаточно для демонстрации преимуществ ведения журнала логов.Выходные данные Eliot
$ python calc.py 12 $ eliot-tree out.log ─── multiplysum (inputs a=1 b=2 c=4) ⧖ 10.0s ├── add (inputs a=1 b=2) ⧖ 1.0s │ └── result: 3 ├── multiply (inputs a=3 b=4) ⧖ 9.0s │ └── result: 12 └── result: 12
multiplysum()
заняло 10 секунд, но больше всего времени было потрачено на multiply()
, с входными данными 3 и 4. Таким образом, вы сразу знаете, что для оптимизации производительности нужно сосредоточиться на multiply()
, и у вас есть некоторые начальные входные данные (3 и 4), чтобы поиграть ими и посмотреть на изменения.Ограничения ведения журнала
4. Правильный выбор инструментов
Всегда добавляйте ведение журнала
После этого используйте профилировщик выборки, такой как Pyinstrument.
Используйте cProfile, если вам нужна индивидуальная метрика затрат
5. TL;DR
Материалы по теме
- 4 views
- 0 Comment