пятница, 27 июля 2012 г.

Руководство по анализу производительности скриптов на Python (Перевод)


Поскольку не каждая программа на Python требует тщательного анализа производительности, отрадно знать, что когда такая необходимость возникает у Вас всё же есть отличные инструменты для этой цели.
Анализ производительности программы состоит в поиске ответа на четыре вопроса:
  1. Как быстро выполняется программа?
  2. Какое место определяет скорость её выполнения?
  3. Как много используется памяти?
  4. Где утечки памяти?
Посмотрим, что же у нас есть для ответа на эти вопросы.

Грубая оценка времени выполнения

Давайте начнём простого и быстрого способа оценки времени выполнения нашего кода: старой доброй утилиты UNIX time:
$ time python yourprogram.py
real 0m1.028s
user 0m0.001s
sys 0m0.003s
Объяснение этого результата можно найти в этой статье. В кратце:
  • real - время от запуска программы до её завершения
  • user - время процессора, затраченное вне ядра
  • sys - время процессора, затраченное в ядре
Таким образом, сложив user и sys, Вы можете оценить сколько процессорного времени потребовалось вашей программе вне зависимости от загрузки системы.
Если эта сумма сильно больше чем реально затраченное время, тогда можно предположить, что проблемы с производительностью вашей программы связаны с ожиданием ввода / вывода.

Более точная оценка времени с использованием менеджера контекста

Вот маленький модуль, который окажет нам бесценную пользу в измерении времени выполнения:
timer.py
importtimeclassTimer(object):def__init__(self,verbose=False):self.verbose=verbosedef__enter__(self):self.start=time.time()returnselfdef__exit__(self,*args):self.end=time.time()self.secs=self.end-self.startself.msecs=self.secs*1000# millisecsifself.verbose:print'elapsed time: %f ms'%self.msecs
Для того, чтобы его использовать, мы должны обернуть тот кусок кода, время выполнения которого Вы хотите замерить в полученный менеджер контекста. Он запускает измерение времени в начале выполнения блока кода и завершает после выполнения. Вот пример использования этого модуля:
fromtimerimportTimerfromredisimportRedisrdb=Redis()withTimer()ast:rdb.lpush("foo","bar")print"=> elasped lpush: %s s"%t.secswithTimerast:rdb.lpop("foo")print"=> elasped lpop: %s s"%t.secs
Я обычно сохраняю вывод в файл для того, чтобы отслеживать изменения производительности с ростом программы.

Построчный тайминг и частота выполнения с помощью profiler

У Robert Kern есть замечательный проект под названием line_profiler, которым я часто пользуюсь для того, чтобы понять, как быстро выполняется каждая строчка моего кода. Чтобы его использовать - Вам надо сперва установить его при помощи pip:
$ pip install line_profiler
После установки можно использовать модуль “line_profiler” или скрипт “kernprof.py”.
Чтобы использовать этот инструмент для начала Вам надо изменить свой код, обернув функцию, производительность которой Вы хотите измерить, в декоратор @profile. Не беспокойтесь, для этого Вам не надо будет ничего импортировать. Скрипт kernprof.py автоматически добавит всё, что нужно в ваш скрипт в процессе выполнения.
primes.py
@profile
def primes(n): 
 if n==2:
 return [2]
 elif n<2:
 return []
 s=range(3,n+1,2)
 mroot = n ** 0.5
 half=(n+1)/2-1
 i=0
 m=3
 while m <= mroot:
 if s[i]:
 j=(m*m-3)/2
 s[j]=0
 while j<half:
 s[j]=0
 j+=m
 i=i+1
 m=2*i+3
 return [2]+[x for x in s if x]
primes(100)
После того, как Вы настроили свой скрипт - используйте usekernprof.py для его запуска.
$ kernprof.py -l -v fib.py
Опция -l указывает kernprof'у добавить декоратор @profile во встроенную область видимости вашего скрипта, а -v указывает kernprof указать информацию о тайминге после выполнения скрипта. Вывод должен выглядеть так:
Wrote profile results to primes.py.lprof
Timer unit: 1e-06 s
File: primes.py
Function: primes at line 2
Total time: 0.00019 s
Line # Hits Time Per Hit % Time Line Contents
==============================================================
 2 @profile
 3 def primes(n): 
 4 1 2 2.0 1.1 if n==2:
 5 return [2]
 6 1 1 1.0 0.5 elif n<2:
 7 return []
 8 1 4 4.0 2.1 s=range(3,n+1,2)
 9 1 10 10.0 5.3 mroot = n ** 0.5
 10 1 2 2.0 1.1 half=(n+1)/2-1
 11 1 1 1.0 0.5 i=0
 12 1 1 1.0 0.5 m=3
 13 5 7 1.4 3.7 while m <= mroot:
 14 4 4 1.0 2.1 if s[i]:
 15 3 4 1.3 2.1 j=(m*m-3)/2
 16 3 4 1.3 2.1 s[j]=0
 17 31 31 1.0 16.3 while j<half:
 18 28 28 1.0 14.7 s[j]=0
 19 28 29 1.0 15.3 j+=m
 20 4 4 1.0 2.1 i=i+1
 21 4 4 1.0 2.1 m=2*i+3
 22 50 54 1.1 28.4 return [2]+[x for x in s if x]
Ищите строки с большим значением hits или time. Это те места, где скрипт можно оптимизировать.

Какое количество памяти мы используем?

Теперь, узнав время выполнения нашего кода, давайте посмотрим на объем используемой им памяти. К счастью для нас, Fabian Pedregosa сделал хороший профилировщик памяти .
Для начала установим его при помощи pip:
$ pip install -U memory_profiler
$ pip install psutil
(Установка пакета psutil сильно ускорит работу memory_profiler).
Как и line_profiler, memory_profiler требует декорирования интересующей вас функции при помощи декоратора @profile:
@profile
def primes(n): 
 ...
 ...
Для того, чтобы посмотреть сколько памяти использует ваша функция запустите скрипт так:
$ python -m memory_profiler primes.py
Вот пример вывода отчёта после завершения работы скрипта:
Filename: primes.py
Line # Mem usage Increment Line Contents
==============================================
 2 @profile
 3 7.9219 MB 0.0000 MB def primes(n): 
 4 7.9219 MB 0.0000 MB if n==2:
 5 return [2]
 6 7.9219 MB 0.0000 MB elif n<2:
 7 return []
 8 7.9219 MB 0.0000 MB s=range(3,n+1,2)
 9 7.9258 MB 0.0039 MB mroot = n ** 0.5
 10 7.9258 MB 0.0000 MB half=(n+1)/2-1
 11 7.9258 MB 0.0000 MB i=0
 12 7.9258 MB 0.0000 MB m=3
 13 7.9297 MB 0.0039 MB while m <= mroot:
 14 7.9297 MB 0.0000 MB if s[i]:
 15 7.9297 MB 0.0000 MB j=(m*m-3)/2
 16 7.9258 MB -0.0039 MB s[j]=0
 17 7.9297 MB 0.0039 MB while j<half:
 18 7.9297 MB 0.0000 MB s[j]=0
 19 7.9297 MB 0.0000 MB j+=m
 20 7.9297 MB 0.0000 MB i=i+1
 21 7.9297 MB 0.0000 MB m=2*i+3
 22 7.9297 MB 0.0000 MB return [2]+[x for x in s if x]

Куда утекает память?

Интерпретатор сPython использует счётчик ссылок для управления памятью. То есть каждый объект содержит счётчик, который увеличивается на 1, когда ссылка на этот объект сохраняется где-то, и уменьшается при удалении этой ссылки. Когда счётчик становится равным нулю интерпретатор cPython знает, что объект больше не используется и можно освободить занимаемую им память.
Утечки памяти могут возникать в вашей программе если где-то сохраняется ссылка на какой-то уже не используемый объект.
Самый быстрый способ поиска таких утечек памяти - использовать восхитительный инструмент objgraph написанный Marius Gedminas. Этот инструмент помогает увидеть количество объектов в памяти и обнаружить разные места в коде, которые содержат ссылки на эти объекты.
Для начала установите objgraph:
pip install objgraph
После этого вставьте в ваш код выражения для вызова отладчика:
import pdb; pdb.set_trace()

Статистика использования объектов

В процессе выполнения Вы можете посмотреть на 20 наиболее используемых объектов в вашей программе:
(pdb) import objgraph
(pdb) objgraph.show_most_common_types()
MyBigFatObject 20000
tuple 16938
function 4310
dict 2790
wrapper_descriptor 1181
builtin_function_or_method 934
weakref 764
list 634
method_descriptor 507
getset_descriptor 451
type 439

Какие объекты были добавлены и удалены?

Для того, чтобы посмотреть, какие объекты были удалены или добавлены между двумя точками сделайте так:
(pdb) import objgraph
(pdb) objgraph.show_growth()
.
.
.
(pdb) objgraph.show_growth() # this only shows objects that has been added or deleted since last show_growth() call
traceback 4 +2
KeyboardInterrupt 1 +1
frame 24 +1
list 667 +1
tuple 16969 +1

Что ссылается на этот "утёкший объект"?

Давайте для примера возьмём простой скрипт:
x = [1]
y = [x, [x], {"a":x}]
import pdb; pdb.set_trace()
Чтобы увидеть, что ссылается на переменную x, вызовите функцию objgraph.show_backref():
(pdb) import objgraph
(pdb) objgraph.show_backref([x], filename="/tmp/backrefs.png")
На выводе Вы получите PNG изображение, сохранённое в/tmp/backrefs.png:
back refrences
Квадрат внизу с красными буквами это и есть интересующий нас объект. Мы можем увидеть, что на него ссылается x один раз и список y три раза. Если x is вызывает утечки памяти, можно воспользоваться этим методом чтобы найти эти забытые ссылки.
Ещё раз: objgraph позволяет нам:
  • посмотреть N популярных объектов в памяти вашей программы
  • посмотреть какие объекты были удалены и добавлены за какой-то период времени
  • посмотреть все ссылки на заданный объект в нашем скрипте

Эффективность против точности

В этом посте я показал несколько инструментов анализа производительности программ на Python. Вооружённые этими инструментами Вы можете получить всю нужную информацию для поиска утечек памяти и бутылочных горлышек в вашем скрипте.
Как и во многих других случаях, точность анализа и скорость его выполнения зависят одно от другого. Так что если Вы сомневаетесь - используйте минимальное решение, удовлетворяющее ваши нужды.
Ссылки
Источник

Комментариев нет:

Отправить комментарий

[フレーム]

AltStyle によって変換されたページ (->オリジナル) /