четверг, 2 февраля 2012 г.

Python: модуль timeit - измерение времени выполнения маленьких фрагментов кода. Примеры использования.

Новое в версии 2.3

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

Интерфейс вызовов

Модуль определяет следующий класс:

class timeit.Timer([stmt='pass'[, setup='pass'[, timer=]]])
Класс для измерения скорости выполнения маленьких фрагментов кода.

Конструктор принимает аргумент stmt содержащий выражение которое будет замерено, дополнительный аргумент setup представляющий собой выражение, выполняемое перед основным выражением. Функция таймера timer является платформозависимой (см. модуль doc string). Выражения могут содержать символы новой строки, если они не содержат многострочных строковых литералов.

Для измерения времени выполнения первого выражения необходимо использовать метод timeit(). Метод repeat() удобен для многократного вызова timeit(), возвращает список с результатами.

Новое в версии 2.6
Параметры stmt и setup могут принимать объекты которые вызываются без аргументов.Это позволит встраивать вызовы в функцию таймер, который будет выполнен по timeit(). Обратите внимание, в этом случае будут произведены дополнительные затраты, из-за вызывов дополнительных функций.
Timer.print_exc([file=None])
Выводит информацию о полученном исключении из выполняемого кода. Приемущество данного метода перед стандартным выводом исключения состоит в том, что будут отображены исходные строки из компилированного шаблона. Дополнительный аргумент file указывает куда направить вывод об исключении, по-умолчанию это стандартный поток ошибок sys.stderr.

Например так выглядит обычное сообщение об ошибке:
import timeit, traceback
setup='a=5'
stmt='b=a/0'
t=timeit.Timer(stmt, setup)
try:
    t.timeit()
except:
    traceback.print_exc()

Результат:

Traceback (most recent call last):
  File "C:\Documents and Settings\User\tmp.py", line 43, in
    t.timeit()
  File "C:\Python26\lib\timeit.py", line 193, in timeit
    timing = self.inner(it, self.timer)
  File "", line 6, in inner
ZeroDivisionError: integer division or modulo by zero

А так будет выглядеть сообщение об ошибке, используя метод класса:

    except:
        t.print_exc()

Результат:

Traceback (most recent call last):
  File "C:\Documents and Settings\User\tmp.py", line 43, in
    t.timeit()
  File "C:\Python26\lib\timeit.py", line 193, in timeit
    timing = self.inner(it, self.timer)
  File "", line 6, in inner
    b=a/0
ZeroDivisionError: integer division or modulo by zero
Timer.timeit([number=1000000])
Эта команда выполнит выражение setup один раз, а затем возвратит время в секундах типа float, которое требуется что бы выполнить основное выражение number раз.
Важно! По-умолчанию timeit() временно отключает сборщик мусора на время измерений. Преимущество этого поведения заключается в том, что независимые измерения становятся более сопоставимыми. Недостатком же является то что сборщик мусора может быть важным компонентом производительности исследуемой функции. В таком случае необходимо включить сборщик мусора, используя выражение setup как показано в примере:
timeit.Timer('for i in xrange(10): oct(i)', 'gc.enable()').timeit()
Timer.repeat([repeat=3[,number=1000000]])
Вызов timeit() c заданным repeat количеством раз, аргумент number передается в timeit().
Очень заманчиво для вычисления среднего и стандартного отклонения от вектора результата и сообщать о них. Однако, это не очень полезно. В типичном случае, наименьшее значение дает нижнюю границу для того что бы понять, как быстро ваш компьютер может выполнить данный фрагмент кода, более высокие значения в результате, как правило, вызваны не различиями в скорости Python, а вмешательством других процессов в точность синхронизации. Так min() результата, наверное, единственное значение, которое должно заинтересовать. После этого, вы должны смотреть на все вектор и применять здравый смысл, а не статистику.

Начиная с версии 2.6 были добавлены 2 удобные функции.


timeit.timeit(stmt[, setup[, timer[, number=1000000]]]])
Создает экземпляр класса Timer, передает в конструктор входные параметры, вызывает метод timeit(), возвращает результат в секундах типа float.

timeit.repeat(stmt[, setup[, timer[, repeat=3[, number=1000000]]]])
Создает экземпляр класса Timer, передает в конструктор входные параметры, вызывает метод repeat(), возвращает список результатов.
Интерфейс командной строки
Использование:
python -m timeit [-n N] [-r N] [-s S] [-t] [-c] [-h] [statement ...]

Список опций:

-n N/--number=N

количество раз выполнения выражения statement


-r N/--repeat=N
количество раз повторного исполнения таймера (по-умолчанию=3)


-s S/--setup=S
инициализирующее выражение, выполняющееся 1 раз (по-умолчанию 'pass')


-t/--time
использование time.time() (по умолчанию на всех платформах кроме Windows)


-c/--clock
использование time.clock() (по-умолчанию на Windows)


-v/--verbose
вывести необработанные результаты измерений


-h/--help
вывести короткое полезное сообщение и выйти

Пример:

C:\Documents and Settings\User>python -m timeit -s "a=1000" -r 5 "reduce(lambda x,y: x+y, xrange(a))"

Результат:

10000 loops, best of 5: 167 usec per loop

Многострочное выражение может быть заданно с помощью символов переноса строки и отступных пробелов, например:

timeit.timeit('for i in xrange(3):\n    y=i+2\n    a=4\n    if a==y:\n        1/0')

... или так:

>>>timeit.timeit("""
... for i in xrange(3):
...     y=i+2
...     a=4
...     if a==y:
...         1/0
... """
)

Если -n не задан, то соответствующее количество циклов рассчитывается путем последовательного перебора степеней 10, пока общее время составит не менее 0,2 секунды.

Таймер по-умолчанию платформозависим. На платформе MS Windows time.clock() имеет точность микросекунд, но точность time.time() составляет 1/60 секунды. На платформе Unix time.clock() имеет точность 1/100 секунды и time.time() является более точным. На других платформах функция стандартного таймера измеряет wall clock time, а не время CPU. Это означает что другие процессы, запущенные на том же компьютере, могут быть не синхронизированы. Лучший способ что бы достичь точного времени является повторение измерений несколько раз и выбрать лучшее время. Для этого подходит опция -r, повторение по-умолчанию происходит 3 раза, этого в большинстве случаев может быть достаточно. На платформе Unix вы можете использовать time.clock() что бы замерить время CPU.

Замечание: Есть определенные базовые издержки, связанные с выполнением оператора передачи. Код здесь не пытается скрыть это, но Вы должны знать об этом. Базовые издержки могут быть измерены, вызывая программу без параметров.
Примеры
Следующий пример покажет, как можно реализовать проверку быстродействия методов класса.
import time, timeit

class Test_class(object):
    def some_slow_method(self, loop_count):
        for i in xrange(loop_count):
            time.sleep(1)

    def some_quick_method(self, loop_count):
        for i in xrange(loop_count):
            time.sleep(0.1)

if __name__=='__main__':
    # Для того что бы воспользоваться классом необходимого модуля, его необходимо импортировать в инициализирующем выражении setup
    setup="""
from __main__ import Test_class
test=Test_class()
"""

    statements=['test.some_slow_method(5)',
    'test.some_slow_method(3)',
    'test.some_quick_method(5)',
    'test.some_quick_method(3)']
    for item in statements:
        print '%s execute in %s seconds'%(item, min(timeit.repeat(item, setup, timeit.default_timer, 3, 1)))

Результат выполнения:

test.some_slow_method(5) execute in 4.99914006752 seconds
test.some_slow_method(3) execute in 2.99965849878 seconds
test.some_quick_method(5) execute in 0.502633834254 seconds
test.some_quick_method(3) execute in 0.301485550227 seconds

Пример сравнения быстродействия получения массива квадратов диапазона чисел:

setup="data=range(1000)"
statements=[
        '[x*x for x in data]',
'''
result=[]
for x in data:
    result.append(x*x)
'''
,
        'map(lambda x: x*x, data)']
for i in statements:
    print timeit.repeat(i,setup,timeit.default_timer, 3,10000)

Результаты:

[0.85908055846317666, 0.85963830564898203, 0.85809925998696102]
[1.7150783079655412, 1.6902710489738482, 1.698589834151278]
[2.1157915758631347, 2.116887527026563, 2.1211939035795169]

6 комментариев:

  1. Этот комментарий был удален администратором блога.

    ОтветитьУдалить
  2. Этот комментарий был удален администратором блога.

    ОтветитьУдалить
  3. Спасибо, отлично все изложено, подробно и понятно. Хорошо бы еще увидеть Ваши записи на эту же тему, но с учетом последних версий Python 3.4 и 3.5.

    ОтветитьУдалить
    Ответы
    1. На данный момент я не перешел на 3 ветку, когда это произойдет не знаю.

      Удалить