В процессе разработки кода и создания конвейеров обработки данных часто возникают компромиссы, которые вы можете сделать между различными реализациями. На ранних стадиях разработки вашего алгоритма беспокоиться о таких вещах может быть контрпродуктивно. Как выразился Дональд Кнут, «Мы должны забыть о небольшой эффективности, скажем, в 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).