Рубрики
Без рубрики

Профилирующий код Python с Line_Profiler

Как только мы отладим, работают, читаемый (и надеюсь, что тестичный) код, это может стать важным для бывшего … Теги с Python.

После того, как мы отладим, работаем, читаемый (и надеюсь, что тестичный) код, может стать важным исчерпывать его более внимательно и попытаться улучшить производительность кода. Прежде чем мы сможем сделать любой прогресс в определении, если наши изменения являются улучшением, нам нужно измерить текущую производительность и посмотреть, где она тратит свое время. В другая статья Я писал об использовании встроенного Python Profiler, CPROFILE. , чтобы получить профиль некоторых Python Code. В этой статье мы посмотрим на другой инструмент под названием Line_Profiler Отказ Результаты из CPROFILE Покажите относительные характеристики каждой функции, называемой из профилирования кода. Из-за этого гранулярность, которую вы видите, не всегда будет явно указывать и изолировать ваши самые медленные строки кода. Иногда CPROFILE не лучший инструмент для использования. Оказывается много типичного кода либо:

  • Сделайте много работы, не вызывая много функций
  • делает функцию звонков, которые в свою очередь сделают Лот других функций вызовы

Оба эти ситуации могут сделать вывод CPROFILE довольно бесполезен или трудно пройти.

Давайте посмотрим на пример. Вот какой-то код, который принимает образец Dataframe имена и номеров телефонов и просто разбивает имя в первое и фамилию. Довольно тривиально (и ужасно неверно для многих реальных наименований мира), но я напишу первый пропуск кода с некоторыми очевидными (или, возможно, не настолько очевидными) проблемы с производительностью.

Примечание. Этот код был выполнен в Python 3.8 с PandaS 1.1.4. Я также использую Faker модуль.

import pandas as pd
import numpy as np

import logging

from faker import Faker

fake = Faker()

logging.basicConfig(level=logging.INFO)

df = pd.DataFrame([(fake.name(), fake.phone_number()) for _ in range(1000)], columns=['name', 'phone'])
df.head(3)

def function_with_issues(df):
    logging.debug(f'called with {df}') 
    def _get_first_name(n):
        return n.split()[0]
    def _get_last_name(n):
        return ' '.join(n.split()[1:])

    df['first'] = df['name'].apply(_get_first_name)
    df['last'] = df['name'].apply(_get_last_name)

    return df

Профилирование с cprofile

Я также предположу, что этот код был протестирован и дает желаемые результаты, то есть в результате Dataframe имеет первые и фамилии по желанию. Сейчас просто осмотр, вы очень хорошо можете обнаружить проблемы с производительностью здесь и планируйте их исправить, но давайте посмотрим, что сделает профилировщик.

import cProfile
with cProfile.Profile() as pf:
    function_with_issues(df)
    pf.print_stats()

20691 function calls (20137 primitive calls) in 0.020 seconds

   Ordered by: standard name

   ncalls tottime percall cumtime percall filename:lineno(function)
        5 0.000 0.000 0.000 0.000 < __array_function__ internals>:2(all)
        4 0.000 0.000 0.000 0.000 < __array_function__ internals>:2(append)
        2 0.000 0.000 0.000 0.000 < __array_function__ internals>:2(atleast_2d)
        2 0.000 0.000 0.000 0.000 < __array_function__ internals>:2(bincount)
       12 0.000 0.000 0.000 0.000 < __array_function__ internals>:2(concatenate)
       17 0.000 0.000 0.000 0.000 < __array_function__ internals>:2(copyto)
        4 0.000 0.000 0.000 0.000 < __array_function__ internals>:2(ndim)
        4 0.000 0.000 0.000 0.000 < __array_function__ internals>:2(prod)
        4 0.000 0.000 0.000 0.000 < __array_function__ internals>:2(ravel)
       33 0.000 0.000 0.000 0.000 :1017(_handle_fromlist)
        1 0.000 0.000 0.020 0.020 :15(function_with_issues)
     1000 0.000 0.000 0.000 0.000 :17(_get_first_name)

Я отрезал вывод, но он продолжается для страниц. Удачи с использованием этого выхода, чтобы выяснить проблему! Вы можете принять эти данные профиля и попытаться отсортировать его и выяснить, что происходит, но по сути, используя Pandas, вы принесли довольно сложный набор модулей, которые будут генерировать Много функционных вызовов. Вывод по умолчанию из CPROFILE. сделать это трудно понять. Где вы начинаете пытаться ускорить это?

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

Вы можете установить его с помощью пипс (В вашем ноутбуке с использованием % PIP или в командной строке с пипс .

%pip install line_profiler

Я нашел Line_Profiler Документация немного запутана, чтобы начать быстро, поэтому я постараюсь заваривать его до базового использования здесь, чтобы вы могли быстро запустить его в своей среде. Как только вы устанавливаете его с PIP, вы должны быть в состоянии использовать его одним из трех способов:

  • Импортировать его использовать профилировщик непосредственно в коде Python
  • Используйте инструмент командной строки ( KernProf ), чтобы запустить код в другом скрипте
  • Используйте IPython/Jupyter Magic ( lprun ) для запуска кода в сеансе или ноутбуке

Я пройду все три из них сейчас.

Запуск Line_Profiler в коде

Первый способ, которым вы могли бы выбрать использовать Line_Profiler непосредственно в коде. Вероятно, это не так, как вы, вероятно, выберете его в конце, но я думаю, что это полезно для понимания механики инструмента. Как только вы импортируете модуль, вы можете сделать экземпляр LineProfiler Как вы бы с CPROFIL. Профиль Отказ Затем вы должны сказать профилировку, которые функционируют в профиль. Наконец, вам необходимо запустить функцию обертки, которая приведет к выполнению этих функций. Затем вы можете распечатать статистику, созданную Profiler (я поощряю вас посмотреть на результаты необработания, поскольку они могут быть довольно многослойными выходами, а форматирование может быть сложно читать здесь).

import line_profiler

lp = line_profiler.LineProfiler()

lp.add_function(function_with_issues)

# this could be any existing function as well, you don't have to write this from scratch
def wrapper_function():
    function_with_issues(df)

wrapper = lp(wrapper_function)
wrapper()

lp.print_stats()

Timer unit: 1e-06 s

Total time: 0.019832 s
File: 
Function: function_with_issues at line 15

Line # Hits Time    Per Hit % Time Line Contents
==============================================================
    15                             def function_with_issues(df):
    16 1    17360.0 17360.0 87.5   logging.debug(f'called with {df}')
    17 1    2.0     2.0      0.0   def _get_first_name(n):
    18 return n.split()[0]
    19 1    1.0     1.0      0.0   def _get_last_name(n):
    20                                 return ' '.join(n.split()[1:])
    21
    22 1     1323.0 1323.0   6.7    df['first'] = df['name'].apply(_get_first_name)
    23 1     1144.0 1144.0   5.8    df['last'] = df['name'].apply(_get_last_name)
    24
    25 1     2.0    2.0      0.0    return df

Результаты, достижения

Теперь вы можете увидеть результаты вашего профиля линии. Вместо того, чтобы рассказывать вам весь базовый код, выполненный вашим несколькими строками кода Panda, вы просто видите, сколько времени тратится на каждую строку. Обратите внимание, что блок таймера находится в верхней части вывода (в этом случае микросекунды), и если вы умножаете время на этот блок, вы получите время, проведенное на этой строке.

Прежде чем мы начнем обсуждать улучшение этих времен, давайте рассмотрим другие способы, которыми вы можете запускать Line_Profiler Отказ

Используя магию

Если вы запускаете код в ноутбуке или сеансе iPython, вы, вероятно, захотите использовать ЛПРУН магия. Просто загрузите Line_Profiler Расширение, а затем вы можете запустить код в одной строке. Просто скажите это, какие функции в профиль используют -f Переключатель и запустите функцию драйвера на одной линии. Есть и другие варианты сброса результатов и запуска целых модулей, вы можете проверить документы или использовать % lprun? В вашем ноутбуке, чтобы прочитать помощь.

%load_ext line_profiler
%lprun -f function_with_issues function_with_issues(df)

Использование KernProf.

Вы также можете запустить Кернпроф Инструмент из командной строки. В этом случае вам просто нужно отметить свой код с декоратором @profile Затем запустите скрипт из командной строки. Это генерирует двоичный файл с данными профиля.

Затем вы можете прочитать данные профиля и выводить результат.

Для приведенного выше примера мы могли бы поставить следующий код в один файл Python (Performance_Line_Profiler.py).

import pandas as pd
import numpy as np

import logging

from faker import Faker

fake = Faker()

@profile
def function_with_issues(df):
    logging.debug(f'called with {df}')
    def _get_first_name(n):
        return n.split()[0]
    def _get_last_name(n):
        return ' '.join(n.split()[1:])

    df['first'] = df['name'].apply(_get_first_name)
    df['last'] = df['name'].apply(_get_last_name)

    return df

if __name__ == ' __main__':
    df = pd.DataFrame([(fake.name(), fake.phone_number()) for _ in range(1000)], columns=['name', 'phone'])
    function_with_issues(df)

Затем вам нужно будет убедиться, что у вас есть среда Python, загруженная в командной строке, где вы установили Line_Profiler используя PIP. Я использую пинв и Виртуальский Чтобы создать определенный VirtualenV, который я активировал и использовал для этого примера. На сеансе с помощью VirtualenV активирована инструмент KERNPROF.

$ kernprof -l performance_line_profiler.py
Wrote profile results to performance_line_profiler.py.lprof

Тогда вы можете просмотреть результаты, как это:

$ python -m line_profiler performance_line_profiler.py.lprof

Вы получите примерно те же результаты, которые вы делали ранее.

Ускорение

Теперь обратно обратно в наши результаты профиля. Сначала мы видим, что код проводит большое количество времени в журнале отладки, даже если вы увидите, что мы не работаем на уровне отладки. Я думал, что регистрация должна была быть по сути не-OP, если вы не бегаете на этом уровне? В чем дело?

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

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

def updated_function(df):
    logging.debug('called with %s', df)

    splits = df['name'].str.split().str
    df['first'] = splits[0]
    df['last'] = splits[1:].str.join(' ')

    return df
%lprun -f updated_function updated_function(df)
Timer unit: 1e-06 s

Total time: 0.006248 s
File: 
Function: updated_function at line 1

Line # Hits Time Per Hit % Time Line Contents
==============================================================
     1 def updated_function(df):
     2 1 13.0    13.0    0.2   logging.debug('called with %s', df)
     3
     4 1 2348.0 2348.0   37.6   splits = df['name'].str.split().str
     5 1 2001.0 2001.0   32.0   df['first'] = splits[0]
     6 1 1885.0 1885.0   30.2   df['last'] = splits[1:].str.join(' ')
     7
     8 1  1.0    1.0      0.0   return df

Что ж, мы сразу видим, что удаление формата готовника было мудрым, теперь он в основном нет-OP. Но оказывается, что этот новый подход не быстрее, чем наш старый метод на основе приложения для этого тестовых данных. Оказывается, создавая временную переменную для хранения результата распределения расходов некоторое время, и доступа к разделенным данным также медленно. Мы могли бы сделать раскол дважды, но все еще медленнее, чем старый подход. Оказывается, использование Применить и String Spliting на значения быстрее, чем использование встроенного Pandas ул ...| Аксессуары. Это было немного сюрпризом для меня, и я уверен, что мы могли бы копаться в этом больше, чтобы понять, почему. Может быть, это другая статья в другое время. Но урок это платит в профиль! Просто укрепить это с гораздо более простым примером, мы можем использовать % Timeit наблюдать за этими различиями для расщепления.

> %timeit df['name'].str.split().str[0]
1.07 ms ± 70.4 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

> %timeit df['name'].apply(lambda x: x.split()[0])
443 µs ± 39.9 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

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

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

Пост Профилирующий код Python с Line_Profiler появился первым на Wrighters.io Отказ

Оригинал: “https://dev.to/wrighter/profiling-python-code-with-lineprofiler-57cf”