- PVSM.RU - https://www.pvsm.ru -
Предположим, ваша Python-программа оказалась медленной, и вы выяснили, что это лишь отчасти обусловлено нехваткой процессорных ресурсов [1]. Как выяснить то, какие части кода вынуждены ожидать чего-то такого, что не относится к CPU?
Прочтя материал, перевод которого мы публикуем сегодня, вы узнаете о том, как писать собственные профилировщики для Python-кода. Речь идёт об инструментах, которые позволят обнаружить места в коде, которые бездействуют в ожидании освобождения неких ресурсов. В частности, мы обсудим здесь следующее:
В те моменты, когда программа не занята интенсивными вычислениями с использованием процессора, она, по всей видимости, чего-то ждёт. Вот чем может быть вызвано бездействие программы:
Как обнаружить те места программ, в которых происходит что-то такое, что плохо влияет на производительность?
Встроенный профилировщик Python, cProfile, умеет собирать данные по множеству различных показателей, имеющих отношение к работе программ. Благодаря этому его можно использовать для создания инструмента, с помощью которого можно проанализировать время, в течение которого программа не пользуется ресурсами процессора.
Операционная система может сообщить [1] нам о том, сколько именно процессорного времени использовала программа.
Представим, что мы профилируем однопоточную программу. Многопоточные программы сложнее профилировать, да и описывать этот процесс тоже непросто. Если программа выполнялась 9 секунд и при этом пользовалась процессором 7.5 секунд — это означает, что она потратила 1.5 секунды на ожидание.
Для начала создадим таймер, который будет измерять время ожидания:
import os
def not_cpu_time():
times = os.times()
return times.elapsed - (times.system + times.user)
Затем создадим профилировщик, который выполняет анализ этого времени:
import cProfile, pstats
def profile_not_cpu_time(f, *args, **kwargs):
prof = cProfile.Profile(not_cpu_time)
prof.runcall(f, *args, **kwargs)
result = pstats.Stats(prof)
result.sort_stats("time")
result.print_stats()
После этого можно профилировать различные функции:
>>> profile_not_cpu_time(
... lambda: urlopen("https://pythonspeed.com").read())
ncalls tottime percall filename:lineno(function)
3 0.050 0.017 _ssl._SSLSocket.read
1 0.040 0.040 _socket.getaddrinfo
1 0.020 0.020 _socket.socket.connect
1 0.010 0.010 _ssl._SSLSocket.do_handshake
342 0.010 0.000 find.str
192 0.010 0.000 append.list
Полученные результаты позволяют сделать вывод о том, что большая часть времени потрачена на чтение данных из сокета, но некоторое время ушло на выполнение DNS-поиска (getaddrinfo), а также на выполнение TCP-рукопожатия (connect) и TLS/SSL-рукопожатия.
Так как мы позаботились о том, чтобы исследовать те периоды работы программы, в которые она не пользуется ресурсами процессора, мы знаем, что всё это — чистое время ожидания, то есть — время, когда программа не занята какими-либо вычислениями.
Почему тут присутствует время, записанное для str.find и list.append? При выполнении подобных операций программе нечего ждать, поэтому правдоподобно выглядит объяснение, в соответствии с которым мы имеем дело с ситуацией, когда не выполнялся весь процесс. Возможно — ожидая окончания выполнения какого-то другого процесса, или ожидая завершения загрузки данных в память из файла подкачки. Это указывает на то, что на выполнение данных операций было затрачено некоторое время, которое не входит в состав процессорного времени.
Кроме того, хочу отметить, что мне доводилось видеть отчёты, в которых имеются небольшие отрицательные фрагменты времени. Это предполагает наличие определённого несоответствия между прошедшим временем и процессорным временем, но я не жду, что это окажет значительное воздействие на анализ более сложных программ.
Проблема с измерением времени, затраченного программой на ожидание чего-либо, заключается в том, что оно, при выполнении разных сеансов измерений для одной и той же программы, может варьироваться из-за чего-то такого, что находится вне сферы действия программы. Иногда DNS-запросы могут оказаться более медленными, чем обычно. Иногда медленнее, чем обычно, могут загружаться какие-то данные. Поэтому полезно было бы использовать некие более предсказуемые показатели, которые не привязаны к скорости работы того, что окружает программу.
Один из способов это сделать заключается в подсчёте того, сколько операций, требующих ожидания, выполнил процесс. То есть — речь идёт о подсчёте количества периодов ожидания, а не времени, которое потрачено на ожидание чего-либо.
Процесс может прекратить пользоваться ресурсами процессора по двум причинам:
Мы будем профилировать преднамеренные переключения контекста.
Напишем профилировщик, который подсчитывает преднамеренные переключения контекста с использованием библиотеки psutil:
import psutil
_current_process = psutil.Process()
def profile_voluntary_switches(f, *args, **kwargs):
prof = cProfile.Profile(
lambda: _current_process.num_ctx_switches().voluntary)
prof.runcall(f, *args, **kwargs)
result = pstats.Stats(prof)
result.sort_stats("time")
result.print_stats()
Теперь снова проведём профилирование кода, который работает с сетью:
>>> profile_voluntary_switches(
... lambda: urlopen("https://pythonspeed.com").read())
ncalls tottime percall filename:lineno(function)
3 7.000 2.333 _ssl._SSLSocket.read
1 2.000 2.000 _ssl._SSLSocket.do_handshake
1 2.000 2.000 _socket.getaddrinfo
1 1.000 1.000 _ssl._SSLContext.set_default_verify_path
1 1.000 1.000 _socket.socket.connect
Теперь, вместо данных о времени ожидания, мы можем видеть сведения о количестве произошедших преднамеренных переключений контекста.
Обратите внимание на то, что иногда можно видеть преднамеренные переключения контекста в неожиданных местах. Я полагаю, что это происходит тогда, когда из-за ошибок страниц памяти выполняется загрузка данных из файла подкачки.
Использование описанной здесь методики профилирования кода создаёт определённую дополнительную нагрузку на систему, что сильно замедляет программу. В большинстве случаев, однако, это не должно привести к значительному искажению результатов из-за того, что мы не выполняем анализ использования ресурсов процессора.
В целом же можно отметить, что профилированию поддаются любые поддающиеся учёту показатели, связанные с работой программы. Например — следующие:
psutil.Process().read_count) и записи (psutil.Process().write_count).Process().read_chars).Подробности по первым двум пунктам этого списка можно найти в документации по psutil [4].
Уважаемые читатели! Как вы профилируете свои Python-приложения?
Автор: ru_vds
Источник [6]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/razrabotka/328398
Ссылки в тексте:
[1] лишь отчасти обусловлено нехваткой процессорных ресурсов: https://pythonspeed.com/articles/blocking-cpu-or-io/
[2] Image: https://habr.com/ru/company/ruvds/blog/464133/
[3] jemalloc: http://jemalloc.net/
[4] psutil: https://psutil.readthedocs.io/en/latest/#processes
[5] Image: https://ruvds.com/vps_start/
[6] Источник: https://habr.com/ru/post/464133/?utm_campaign=464133&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.