You are currently viewing Код профилирования и синхронизации

Код профилирования и синхронизации

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

Но как только ваш код заработает, может быть полезно немного разобраться в его эффективности. Иногда полезно проверить время выполнения данной команды или набора команд; в других случаях полезно углубиться в многострочный процесс и определить, где находится узкое место в какой-либо сложной серии операций. IPython предоставляет доступ к широкому спектру функциональных возможностей для такого рода синхронизации и профилирования кода. Здесь мы обсудим следующие магические команды IPython:

  • %time: Время выполнения одного оператора
  • %timeit: Время повторного выполнения одного оператора для большей точности
  • %prun: Запустите код с помощью профилировщика
  • %lprun: Запустите код с помощью построчного профилировщика
  • %memit: Измерьте использование памяти одним оператором.
  • %mprun: Запустите код с помощью построчного профилировщика памяти

Последние четыре команды не входят в комплект поставки IPython–вам нужно будет получить расширения line_profilerиmemory_profiler, которые мы обсудим в следующих разделах.

Фрагменты кода синхронизации: %timeit и %time

Мы видели %timeit магию строк и %%timeit магию ячеек во введении к магическим функциям в магических командах IPython; ее можно использовать для определения времени повторного выполнения фрагментов кода: В [1]:

%timeit sum(range(100))
100000 loops, best of 3: 1.54 µs per loop

Обратите внимание, что из-за того, что эта операция выполняется так быстро, %timeit автоматически выполняется большое количество повторений. Для более медленных команд %timeit автоматически настраивается и выполняет меньше повторений: В [2]:

%%timeit
total = 0
for i in range(1000):
    for j in range(1000):
        total += i * (-1) ** j
1 loops, best of 3: 407 ms per loop

Иногда повторение операции-не лучший вариант. Например, если у нас есть список, который мы хотели бы отсортировать, повторная операция может ввести нас в заблуждение. Сортировка предварительно отсортированного списка выполняется намного быстрее, чем сортировка несортированного списка, поэтому повторение приведет к искажению результата: В [3]:

import random
L = [random.random() for i in range(100000)]
%timeit L.sort()
100 loops, best of 3: 1.9 ms per loop

Для этого функция %time magic может быть лучшим выбором. Это также хороший выбор для более длительных команд, когда короткие задержки, связанные с системой, вряд ли повлияют на результат. Давайте определим время сортировки несортированного и предварительно отсортированного списка: В [4]:

import random
L = [random.random() for i in range(100000)]
print("sorting an unsorted list:")
%time L.sort()
sorting an unsorted list:
CPU times: user 40.6 ms, sys: 896 µs, total: 41.5 ms
Wall time: 41.5 ms

В [5]:

print("sorting an already sorted list:")
%time L.sort()
sorting an already sorted list:
CPU times: user 8.18 ms, sys: 10 µs, total: 8.19 ms
Wall time: 8.24 ms

Обратите внимание, насколько быстрее выполняется сортировка предварительно отсортированного списка , но также обратите внимание, сколько времени занимает %time сравнение %timeit даже для предварительно отсортированного списка! Это является результатом того факта, что %timeit он делает некоторые хитрые вещи под колпаком, чтобы предотвратить вмешательство системных вызовов в синхронизацию. Например, это предотвращает очистку неиспользуемых объектов Python (известных как сборка мусора), которые в противном случае могли бы повлиять на время. По этой причине %timeit результаты обычно заметно быстрее, чем %time результаты.

%time Как и в случае с %timeit, использование синтаксиса магии ячеек с двойным знаком процента позволяет синхронизировать многострочные сценарии: В [6]:

%%time
total = 0
for i in range(1000):
    for j in range(1000):
        total += i * (-1) ** j
CPU times: user 504 ms, sys: 979 µs, total: 505 ms
Wall time: 505 ms

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

Профилирование Полных Сценариев: %prun

Программа состоит из множества отдельных утверждений, и иногда выбор времени для этих утверждений в контексте более важен, чем выбор времени для них самих. Python содержит встроенный профилировщик кода (о котором вы можете прочитать в документации по Python), но IPython предлагает гораздо более удобный способ использования этого профилировщика в виде функции magic %prun.

В качестве примера мы определим простую функцию, которая выполняет некоторые вычисления: В [7]:

def sum_of_lists(N):
    всего = 0
    для i в диапазоне(5):
def sum_of_lists(N):
    total = 0
    for i in range(5):
        L = [j ^ (j >> i) for j in range(N)]
        total += sum(L)
    return total

Теперь мы можем позвонить %prun с помощью вызова функции, чтобы увидеть профилированные результаты: В [8]:

%prun sum_of_lists(1000000)

В записной книжке вывод выводится на пейджер и выглядит примерно так:

14 function calls in 0.714 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        5    0.599    0.120    0.599    0.120 <ipython-input-19>:4(<listcomp>)
        5    0.064    0.013    0.064    0.013 {built-in method sum}
        1    0.036    0.036    0.699    0.699 <ipython-input-19>:1(sum_of_lists)
        1    0.014    0.014    0.714    0.714 <string>:1(<module>)
        1    0.000    0.000    0.714    0.714 {built-in method exec}

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

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

Построчное профилирование с%lprun

Профилирование по функциям %prun полезно, но иногда удобнее иметь отчет о профиле по строкам. Это не встроено в Python или IPython, но line_profiler для установки доступен пакет, который может это сделать. Начните с использования инструмента упаковки Python, pip, чтобы установить line_profiler пакет:

$ pip install line_profiler

Затем вы можете использовать IPython для загрузки расширения line_profiler IPython, предлагаемого в рамках этого пакета: В [9]:

%load_ext line_profiler

Теперь %lprun команда будет выполнять построчное профилирование любой функции-в этом случае нам нужно явно указать, какие функции нас интересуют для профилирования: В [10]:

%lprun -f sum_of_lists sum_of_lists(5000)

Как и прежде, записная книжка отправляет результат на пейджер, но выглядит это примерно так:

Timer unit: 1e-06 s

Total time: 0.009382 s
File: <ipython-input-19-fa2be176cc3e>
Function: sum_of_lists at line 1

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
     1                                           def sum_of_lists(N):
     2         1            2      2.0      0.0      total = 0
     3         6            8      1.3      0.1      for i in range(5):
     4         5         9001   1800.2     95.9          L = [j ^ (j >> i) for j in range(N)]
     5         5          371     74.2      4.0          total += sum(L)
     6         1            0      0.0      0.0      return total

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

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

Использование памяти профилирования: %memit и %mprun

Другим аспектом профилирования является объем используемой операцией памяти. Это можно оценить с помощью другого расширения IPython, memory_profiler. Как и в случае с line_profiler, мы начинаем с pip установки расширения:

$ pip install memory_profiler

Затем мы можем использовать IPython для загрузки расширения: В [12]:

%load_ext memory_profiler

Расширение профилировщика памяти содержит две полезные магические функции: %memit магию (которая предлагает эквивалент измерения памяти %timeit) и %mprun функцию (которая предлагает эквивалент измерения памяти %lprun). Эту %memit функцию можно использовать довольно просто: В [13]:

%memit sum_of_lists(1000000)
peak memory: 100.08 MiB, increment: 61.36 MiB

Мы видим, что эта функция использует около 100 МБ памяти.

Для построчного описания использования памяти мы можем использовать %mprun магию. К сожалению, это волшебство работает только для функций, определенных в отдельных модулях , а не в самом ноутбуке, поэтому мы начнем с использования %%file магии для создания простого модуля под названием mprun_demo.py, который содержит нашу sum_of_lists функцию, с одним дополнением, которое сделает наши результаты профилирования памяти более четкими: В [14]:

%%file mprun_demo.py
def sum_of_lists(N):
    total = 0
    for i in range(5):
        L = [j ^ (j >> i) for j in range(N)]
        total += sum(L)
        del L # remove reference to L
    return total
Overwriting mprun_demo.py

Теперь мы можем импортировать новую версию этой функции и запустить профилировщик строк памяти: В [15]:

from mprun_demo import sum_of_lists
%mprun -f sum_of_lists sum_of_lists(1000000)

Результат, распечатанный на пейджере, дает нам краткое описание использования памяти этой функцией и выглядит примерно так:

Filename: ./mprun_demo.py

Line #    Mem usage    Increment   Line Contents
================================================
     4     71.9 MiB      0.0 MiB           L = [j ^ (j >> i) for j in range(N)]


Filename: ./mprun_demo.py

Line #    Mem usage    Increment   Line Contents
================================================
     1     39.0 MiB      0.0 MiB   def sum_of_lists(N):
     2     39.0 MiB      0.0 MiB       total = 0
     3     46.5 MiB      7.5 MiB       for i in range(5):
     4     71.9 MiB     25.4 MiB           L = [j ^ (j >> i) for j in range(N)]
     5     71.9 MiB      0.0 MiB           total += sum(L)
     6     46.5 MiB    -25.4 MiB           del L # remove reference to L
     7     39.1 MiB     -7.4 MiB       return total

Здесь в Increment столбце указано , насколько каждая строка влияет на общий бюджет памяти: обратите внимание, что при создании и удалении списка L мы добавляем около 25 МБ памяти. Это в дополнение к использованию фоновой памяти самим интерпретатором Python.

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