понедельник, 27 февраля 2012 г.

Измерение интервалов времени

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

time() и clock()

Очень долго их было ровно две: time.time() и time.clock().

Использование предельно простое:

t1 = time.time()
# do tested stuff
print(time.time() - t1)

Получаем время выполнения кода в секундах.

Две функции нужны для того, чтобы усложнить жизнь программиста. Дело в том, что на Windows точнее работает функция time.clock, а на прочих Unix/Linux — time.time.

Дальнейшее касается Linux и (отчасти) других Posix систем. Все нововведения появятся в грядущем Python 3.3

clock_gettime(clk_id)

Вот уже много лет time.time использует системный вызов gettimeofday(2) на тех ОС, где он доступен (Linux, например). gettimeofday(2) позволяет измерять время с точностью до микросекунд в отличие от посекундного time(2).

Прогресс не стоит на месте, и появились функции c наносекундным разрешением: time.clock_gettime и time.clock_getres являются тонкими обёртками над одноимёнными Posix вызовами.

Зачем вообще нужны эти наносекунды и почему не хватает микросекундных интервалов?

Вот вполне жизненный пример: мы используем Redis для кеширования. Он работает на той же машине, где и основное приложение. Используются unix sockets и pipelining. В результате время обработки запросов сопоставимо с 1 мс. Если мы хотим знать, какую часть времени приложение тратит на общение с redis — нужно измерять интервалы с более высокой точностью.

clock_gettime возвращает время, а clock_getres показывает, с какой точностью оперирует clock_gettime.

Не вполне ясно, какой тип таймера выбирать. Вот список таймеров, имеющихся в ядре Linux 3:

  • CLOCK_REALTIME — абсолютное системное время.

  • CLOCK_MONOTONIC — относительное время.

  • CLOCK_MONOTONIC_RAW — относительное время без коррекции NTP.

  • CLOCK_PROCESS_CPUTIME_ID — высокоточный таймер процесса.

  • CLOCK_THREAD_CPUTIME_ID — высокоточный таймер потока.

Во первых, возвращаемое время может быть абсолютным и относительным. Если по абсолютному времени можно узнать «который час», то относительное годится лишь для измерения интервалов. Относительное время отсчитывается от какой-то точки (для простоты пусть это будет момент включения компьютера), абсолютное традиционно начинается 1 января 1970 года (UNIX Epoch).

Поскольку Питон возвращает результат clock_gettime как float, то наносекундная точность возможна лишь в течении 194 дней от UNIX Epoch timestamp, микросекудная точность «заканчивается» за 272 года. 100 наносекндная точность исчерпывается за 17 лет. Вывод простой: для измерения интервалов с точностью выше микросекунды пригодно только относительное время.

Абсолютное время можно менять путём ручной или автоматизированной (службой NTP) подстройки компьютерных часов. Относительное при этом не изменяется.

На относительное время может оказывать влияние автоматическая коррекция времени (NTP), которая за сутки сдвигает системное время на доли секунды относительно аппаратных компьютерных часов. Когда работаем с наносекундными интервалами — это имеет значение. CLOCK_MONOTONIC_RAW лишён такого недостатка.

И, наконец, есть таймеры, берущие значение из счётчиков процессора. Эти счётчики работают с очень высоким разрешением, да вот беда — в симметричных многопроцессорных системах (SMP) счётчики могут быть не связаны друг с другом. Процесс исполняется то на одном процессоре, то на другом, счётчики выдают несвязанные данные по которым нельзя ничего измерить. Широко распространённые однопроцессорные многоядерные системы этим недостатком не страдают. Простой совет: никогда не используете эти таймеры счётчиков процесса, не поняв до конца всех деталей. Для практических целей обычно достаточно менее экзотичных вариантов.

Перечисленные выше типы таймеров не очень полезны «обычному программисту»: совершенно непонятно, какой таймер выбрать прямо здесь и сейчас.

time.monotonic() и time.wallclock()

Обычно желание простое: измерить интервал времени с максимально возможной точностью, причём способ не должен зависеть от установленной операционной системы и используемого «железа».

Вот тут и приходят на помощь функции time.monotonic() и time.wallclock(). Они не требуют параметров, работают везде и используют максимально точный из доступных таймеров. Для Windows это clock(), на Linux последовательно перебираются CLOCK_MONOTONIC_RAW, CLOCK_MONOTONIC и CLOCK_REALTIME. Кто первый отзовётся — тот и лучший.

Разница между функциями незначительная: time.wallclock() попытается перейти к таймеру абсолютного времени если не найдет лучший вариант (и никогда не закончится исключением), time.monotonic() выбросит OSError.

Итог

На самом деле, думаю, ни на одной современной системе до крайности не дойдет и можно никогда не ощутить разницу на себе. И всё же time.wallclock() чуть-чуть больше похожа на то «стандартное поведение», которое требуется от функции для измерения временных интервалов. Если вам нужно оценить время выполнения чего-либо — используйте её.

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

  1. Очень интересный блог. Пишите чаще.

    ОтветитьУдалить
  2. > Процесс исполняется то на одном процессоре, то на другом, счётчики выдают несвязанные данные по которым нельзя ничего измерить

    Еще не стоит забывать, что тактовая частота современных процессоров довольно часто меняется, особенно мобильных процессоров. Операционная система может изменять ее для всего кристалла или для отдельных ядер до 2х-5ти раз или вообще "усыплять" ядра если на них ничего не исполняется.
    В линуксе эти все проблемы обходятся привязыванием процесса к определенному ядру или группе ядер через cgroups и временным запретом cpu power management. Эти действия очень желательны для любых тестов производительности.

    ОтветитьУдалить
  3. Еще один повод не использовать CPUTIME, не разобравшись полностью во всех деталях.
    Спасибо.

    ОтветитьУдалить
  4. На самом деле статья немного устарела. См. PEP-418: http://www.python.org/dev/peps/pep-0418/ (который, к слову, сейчас правится практически ежедневно).

    ОтветитьУдалить