Время Perl



    Perl и CPAN предоставляют множество самых разных инструментов для работы с временем. Традиционный и наиболее известный DateTime вызывает столь же традиционные серьёзные нарекания к скорости работы и потреблению памяти, поэтому он постепенно стал вытесняться из нашей системы альтернативными модулями. TIMTOWDI — это замечательно, но в проекте всё-таки хочется иметь какой-никакой порядок. Поэтому мы решили протестировать несколько самых популярных модулей по скорости, функционалу и удобству использования и выбрать тот самый единственный, который станет нашим основным инструментом.

    Начальные условия


    Прежде чем проводить тесты, нужно было определиться с требованиями к модулям. Для нашего проекта были поставлены следующие условия.

    Необходимый функционал:
    • работа с временными зонами (вычисления с зонами, определение локальной);
    • парсинг и форматирование строки с временем по шаблону;
    • работа с интервалами времени (прибавить день, отнять неделю, посчитать разницу между датами);
    • работа с такими понятиями как «первая неделя октября» или «двенадцатая неделя года» и т. п.

    Дополнительные условия:
    • желательно один объект времени, с которым можно было бы работать;
    • лаконичный код;
    • модуль не должен быть заброшенным (давно не обновлялся, множество незакрытых багов);
    • работа в Debian со стандартным Perl (сейчас это v5.14.2).

    Критерии оценки:
    • соответствие требованиям по функционалу;
    • скорость работы;
    • адекватный интерфейс;
    • субъективное удобство использования.

    Модули


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

    Я сделаю краткий обзор возможностей и особенностей, влияющих на результаты тестов, и приведу результаты замеров. Подробное описание каждого модуля можно найти в родной документации.

    DateTime


    Наверное, наиболее популярный модуль. Точнее, даже группа модулей, т. к. разный функционал разделён по разным пространствам имён. В целом функционал DateTime очень широк, но модуль часто подвергается критике за низкую производительность.

    При создании нового объекта учитывается время с точностью до секунды, но работать может с точностью до наносекунд.

    Парсить строки с датами сам по себе не умеет, но есть множество готовых парсеров (форматтеров), DateTime::Format::*. Они же используются и для формирования строки с временем в необходимом формате, если реализуют метод format_datetime. Для тестов я буду использовать DateTime::Format::ISO8601 (чаще используется в разных API и сервисах) и DateTime::Format::Strptime (позволяет использовать свой шаблон, аналогичный strptime). Также можно создать свой собственный парсер с помощью DateTime::Format::Builder.

    Для работы с временными зонами можно использовать объекты DateTime::TimeZone, но это необязательно. Например, создать объект времени в определённой зоне можно просто указав time_zone => 'Asia/Taipei'. Важно понимать, что описание зон находится в самом модуле и за их актуальностью нужно следить отдельно. Также можно вместо строки передать заранее подготовленный объект DateTime::TimeZone, что может быть полезно, когда мы используем локальную временную зону. Определение её может быть долгим и эффективнее заранее подготовить объект, например, так:

    state $tz = DateTime::TimeZone->new( name => 'local' );
    

    Для работы с интервалами используются объекты DateTime::Duration, эти же объекты возвращаются при вычитании дат.

    Сравнение дат можно выполнять как с учетом долей секунд, так и по целым значениям, используя соответствующие методы DateTime->compare( $dt1, $dt2 ) и DateTime->compare_ignore_floating( $dt1, $dt2 ).

    В целом интерфейс модуля мне кажется достаточно простым и понятным. Использование множества модулей может кому-то показаться неудобным, но организованы они грамотно и я не могу назвать это недостатком.

    # Пустой объект создавать не умеет, для new всегда нужны параметры.
    my $dt = DateTime->new(
        year       => 1964,
        month      => 10,
        day        => 16,
        hour       => 16,
        minute     => 12,
        second     => 47,
        nanosecond => 500_000_000,
        time_zone  => 'Asia/Taipei',
    );
    
    # Объект с текущим локальным временем
    $dt = DateTime->now();
    
    # Объект с текущим временем UTC
    $dt = DateTime->now( time_zone => 'UTC' );
    
    # Объект с текущим временем в заданной временной зоне
    $dt = DateTime->now( time_zone => '+1000' );
    
    # Парсинг строки (ISO8601)
    $dt = DateTime::Format::ISO8601->parse_datetime('2015-02-18T10:50:31.521345123+10:00');
    
    # Парсинг строки по шаблону (медленнее, чем ISO8601)
    my $dt_format = DateTime::Format::Strptime->new(
        pattern  => '%Y-%m-%dT%H:%M:%S.%9N%z',
        on_error => 'croak',
    );
    $dt = $dt_format->parse_datetime('2015-02-18T10:50:31.521345123+1000');
    
    # Генерация строки по шаблону
    my $str = $dt_format->format_datetime($dt);
    
    # Прибавить 1 год 2 месяца 3 дня 4 часа 5 минут и 6 секунд
    my $dt_duration = DateTime::Duration->new(
        years   => 1,
        months  => 2,
        days    => 3,
        hours   => 4,
        minutes => 5,
        seconds => 6,
    );
    my $dt2 = $dt + $dt_duration;
    
    # Сравнить даты
    my $result = DateTime->compare( $dt, $dt2 ); # результат: -1 т. к. $dt < $dt2
    
    # Интервал между датами
    my $interval = $dt2->subtract_datetime( $dt1 );
    
    # Определение начала / конца недели и месяца
    my $week_begin  = $dt->clone->truncate( to => 'week' );
    my $week_end    = $week_begin->clone->add( days => 6 );
    my $month_begin = $dt->clone->truncate( to => 'month' );
    my $month_end   = $month_begin->clone->add( months => 1 )->subtract( days => 1 );
    

    Date::Manip


    Главной особенностью этого модуля я бы назвал всеядность. Он умеет делать весьма хитрые манипуляции, например, определять дату по строке «8:00pm December tenth», и даже на разных языках. Но документация к этому модулю — самая непонятная (по крайней мере мне). Подобно DateTime, функционал разделён по множеству модулей, но логика их разделения не очевидна. Для того, чтобы создать новый объект, приходится использовать документацию сразу к трём модулям — Date::Manip, Date::Manip::Date, Date::Manip::Obj.

    С долями секунды работать не умеет, хотя это и не всегда необходимо, но для кого-то может оказаться критичным.

    Умеет парсить строковые даты вроде «8:00pm December tenth» или «4 business days later», ещё и на разных языках. Это очень круто, но я лично с такой задачей никогда не сталкивался. Наверное, это имеет смысл при работе с каким-то слабостандартизированным (или намеренно очень свободным) пользовательским вводом.

    Интервалы представлены объектами Date::Manip::Delta, они же возвращаются как разность между датами.

    Для сравнения дат используется специальный метод:

    $dm_date1->cmp( $dm_date2 );
    

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

    Не умеет создавать копию существующего объекта.

    В целом интерфейс довольно специфичный. Наверное, к нему можно привыкнуть, и тогда он будет казаться более красивым (как с ObjectiveC), но нужно ли это делать?

    # Новые пустые объекты. Отдельно для модуля и отдельно для даты, рекомендуется именно так,
    # потому что внутри создаются и переиспользуются базовые объекты.
    my $dm = Date::Manip::Date->new;
    my $dt = $dm->new_date;
    
    # Текущее локальное время. Только через парсинг.
    $dt->parse('now');
    
    # Текущее время UTC
    $dt->parse('now gmt');
    
    # Время в заданной зоне
    $date->parse('now gtm+10');
    
    # Парсинг строки (ISO8601)
    $date->parse('2015-02-18T10:50:31.521345123+10:00');
    
    # Генерация строки по шаблону
    my $str = $dm_date->printf("%Y.%m.%d %H-%M-%S %z");
    
    # Прибавить 1 год 2 месяца 3 дня 4 часа 5 минут и 6 секунд
    my $dm_delta = Date::Manip::Delta->new;
    $dm_delta->parse('1:2:0:3:4:5:6'); # 0 — недели
    my $dm_date2 = $dm_date->calc( $dm_delta );
    
    # Сравнить даты
    my $result = $dm_date1->cmp( $dm_date2 ); # -1
    
    # Интервал между датами
    my $interval = $dm_date2->calc( $dm_date1 );
    
    # Определение начала и конца текущих недели и месяца
    # (каждый раз использую parse('now') т. к. не знаю, как копировать готовый объект)
    my $week_begin = $dm_date->new_date;
    $week_begin->parse('now');
    $week_begin->prev(1,1,[0,0,0]); # Перевести дату на понедельник 00:00:00
    # первый аргумент значит, что ищем понедельник (1-й день недели)
    # второй значит, что текущий день тоже считается
    # третий - время дня (ч,м,с)
    
    my $week_end = $dm_date->new_date;
    $week_end->parse('now');
    $week_end->prev(7,1,[0,0,0]);
    
    my $month_begin = $dm_date->new_date;
    $month_begin->parse('now');
    $month_begin->set('time',[0,0,0]);
    $month_begin->set('d',1);
    
    my $delta = Date::Manip::Delta->new;
    $delta->parse('0:1:0:-1:0:0:0');
    my $month_end = $dm_m1->calc( $delta );
    

    Time::Piece


    Ещё один очень популярный модуль. К тому же, это core-модуль, входящий в поставку Perl. По сути своей является ОО-оберткой над стандартными функциями. По умолчанию перекрывает localtime и gmtime. Пустой объект создавать не умеет, при вызове new делает то же самое, что и localtime, но если передан другой объект Time::Piece, то создаёт его копию.

    Не умеет менять временную зону созданного объекта, но время в нужной зоне можно получить по смещению (в секундах).

    Парсить умеет только по шаблону strptime, что достаточно в большинстве случаев.

    С долями секунды работать не умеет.

    Все вычисления производятся с секундами. Для удобства есть предустановленные константы из Time::Seconds (ONE_DAY, например). Результаты вычислений получаются не совсем очевидные, так '2015-02-25 10:33:25' + ONE_YEAR = '2016-02-25 16:22:15'. Все дело в том, что ONE_YEAR это 31556930 секунд или 365.24225 дня (да, с округлением). C месяцем то же самое: '2015-02-01 00:00:00' + ONE_MONTH = '2015-03-03 10:29:04'. Автор, понимая эту проблему, предусмотрел два метода объекта: add_months и add_years. Но работают они тоже с особенностями: отняв месяц от 2008-03-31, мы получим 2008-03-02. Это нужно всегда помнить и учитывать.

    Можно работать с объектами, используя стандартные арифметические операторы и операторы сравнения: -, +=, <, >=, <=> и т. п.

    В качестве разности дат возвращается объект Time::Seconds.

    Не умеет менять день недели и месяца, только определять. В последнем тесте (определение начала и конца недели и месяца) часть работы приходится делать руками, что не очень удобно.

    В целом интерфейс достаточно простой, понятный и без излишеств. Во многих случаях его будет достаточно.

    # Текущее локальное время. Можно через ->new или через localtime
    my $tp = Time::Piece->new;
    $tp = localtime;
    
    # Текущее время UTC
    $tp = gmtime;
    
    # Смещение в заданную зону делается простым сложением.
    $tp += 60*60*10; # тут смещение в секундах
    
    # Парсинг строки (по заданному шаблону)
    $tp = Time::Piece->strptime('2015-02-18T10:50:31+1000', '%Y-%m-%dT%H:%M:%S%z');
    
    # Генерация строки по шаблону
    my $str = $tp->strftime("%Y.%m.%d %H-%M-%S %z");
    
    # Прибавить 1 год 2 месяца 3 дня 4 часа 5 минут и 6 секунд
    my $tp2 = $tp1 + 3 * ONE_DAY + 4 * ONE_HOUR + 5 * ONE_MINUTE + 6;
    $tp2->add_years(1);
    $tp2->add_months(2);
    
    # Сравнить даты
    my $result = $tp1 <=> $tp2; # -1
    
    # Вычитание
    my $interval_in_seconds = $tp1 - $tp2;
    

    Panda::Date


    Модуль написан с использованием XS и позиционируется как очень быстрый. Имеет существенное ограничение — для сборки требуется Perl 5.18 или выше.

    По умолчанию для парсинга принимает строки вида '2013-03-05 23:45:56'. Но можно задать и другой формат (глобально):

    Panda::Date::string_format("%Y%m%d%H%M%S");
    

    При парсинге создаются объекты с локальной временной зоной.

    С долями секунды работать не умеет.

    Может производить вычисления без использования дополнительных объектов (складывать даты с датам, а не с интервалами), но можно использовать и объекты Panda::Date::Int. Умеет прибавлять строки вида '3Y 2D' (3 года и 2 дня) или объекты типа Panda::Date::Rel, такое сложение работает даже быстрее, чем сложение с ARRAYREF. При вычитании дат возвращает объект Panda::Date::Int.

    Для сравнения используется только оператор <=>.

    Позволяет манипулировать днём недели через day_of_week, при этом неделя начинается с воскресенья (значение 0). Или можно использовать ewday, тогда неделя начинается с понедельника (значение 1) и заканчивается воскресеньем (7).

    # Пустой объект создать нельзя
    # Можно создать объект с текущим временем тремя разными способами
    my $pd = Panda::Date->new;
    my $pd = Panda::Date->new( time );
    my $pd = now; # импортировано по умолчанию
    
    # При этом скорость у всех разная. Результат теста
    #                          Rate Panda::Date new(time) Panda::Date new Panda::Date now
    # Panda::Date new(time) 742853/s                    --             -9%            -23%
    # Panda::Date new       813840/s                   10%              --            -16%
    # Panda::Date now       967947/s                   30%             19%              --
    
    # Текущее локальное время (рекомендуется использовать now).
    $pd = now;
    
    # Текущее время UTC
    $pd = Panda::Date->new( time, 'UTC' );
    
    # Время в заданной зоне
    $pd->to_tz('UTC-10');
    
    # Парсинг строки (только в заданном глобально формате)
    $pd = Panda::Date->new('2015-02-18 10:50:31');
    
    # Генерация строки по шаблону
    my $str = $pd->strftime("%Y.%m.%d %H-%M-%S %z");
    
    # Прибавить 1 год 2 месяца 3 дня 4 часа 5 минут и 6 секунд
    my $pd1 = Panda::Date::now;
    my $pd2 = $pd1 + [1,2,3,4,5,6];
    my $pd2 = $pd1 + '1Y 2M 3D 4h 5m 6s'; # работает быстрее
    
    # Сравнить даты
    my $result = $pd1 <=> $pd2;
    
    # Вычитание дат
    my $interval = $pd1 - $pd2;
    

    Date::Calc


    Ещё один популярный модуль. Отличительной особенностью его является то, что он использует простой массив для хранения информации о дате и времени вместо специального объекта. Интерфейс достаточно прост и понятен. Много разных функций для манипуляций с датами, чуть меньше для манипуляций с временем. Работает с точностью до секунд.

    В отличие от других модулей, не имеет встроенного механизма для генерации строк с временем, а парсить умеет только даты, и то в специфичном формате. Поэтому в тестах на парсинг и генерацию строк не участвует.

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

    # Получение локальных даты и времени
    my @dc = Today_and_Now();
    
    # Получение времени UTC осуществляется передачей дополнительного параметра.
    @dc = Today_and_Now(1);
    
    # Для определения времени в том или ином часовом поясе можно использовать время UTC и смещение
    # на необходимое количество часов
    my @delta_tz = (0, 10, 0, 0); # дни, часы, минуты, секунды
    my @dc_tz =Add_Delta_DHMS( Today_and_Now(1), @delta_tz );
    
    # Прибавить 1 год 2 месяца 3 дня 4 часа 5 минут и 6 секунд
    @dc = Add_Delta_YMDHMS( @dc, (1,2,3,4,5,6) );
    
    # Сравнить даты
    my @dc1 = (2015,2,18,10,50,31);
    my @dc2 = (2015,2,18,10,50,32);
    my $result = 0+Delta_DHMS( @dc1, @dc2 );     # положительный результат означает,
                                                 # что дата 2 больше даты 1
    
    # Вычитание
    my @interval = Delta_YMDHMS( @dc1, @dc2 );
    
    # Определение начала / конца недели и месяца. Многое нужно делать вручную.
    @dc =Today(); # получаем сразу без времени
    my $dow =Day_of_Week( @dc );
    my @week_begin =Add_Delta_Days( @dc, (1 - $dow) );
    my @week_end =Add_Delta_Days( @week_begin, 6 );
    my @month_begin = @dc;
    $month_begin[2] = 1; # просто меняем день месяца
    my @month_end =Add_Delta_Days( Add_Delta_YMD( @month_begin, (0,1,0) ), -1 );
    

    Time::Moment


    Достаточно новый модуль (текущая версия — 0.22), я узнал о нём только когда начал готовить эту статью.

    Умеет работать с наносекундами, но по умолчанию создаёт объект с временем (локальное и UTC) с точностью до микросекунд.

    Может парсить даты только в строго определённом формате — ISO 8601. Что может быть не очень удобно. При ошибке парсинга выкидывает исключение.

    Может выполнять простые операции ± год / месяц / день и т. д. через специальные методы (plus_years например). В отличие от многих других модулей, при вычислении 2013-01-31 + 1 месяц даёт результат 2013-02-28. Что правильно, на мой взгляд. Хотя, возможно, кто-то ожидает и другого поведения.

    Для сравнения дат использует стандартные операторы сравнения чисел: <=>, ==, >= и т. д.

    Нет методов для определения интервала между двумя датами. Но можно выполнить вычитание секунд с начала эпохи, полученных методом epoch. Это накладывает ограничения и теряется точность, но в каких-то случаях такой точности может быть достаточно (Time::Piece тоже ведь с секундами работает).

    Для определения начала / конца месяца / недели можно использовать методы with_day_of_week (понедельник 1, воскресенье 7), with_day_of_month и математику. Для обнуления времени суток приходится использовать методы with_*.

    В целом интерфейс очень прост, понятен и без заметных специфических особенностей (как с математикой у Time::Piece, например).

    # Создать пустой объект
    my $tm = Time::Moment->new;
    
    # Создать объект с текущим локальным временем
    $tm = Time::Moment->now;
    
    # Создать объект с текущим временем UTC
    $tm = Time::Moment->now_utc;
    
    # Время в определённой зоне (заданной смещением в минутах)
    my $tm_with_offset = $tm->with_offset_same_instant(600); # тут смещение в минутах
    
    # Парсинг строки (в формате ISO8601)
    $tm = Time::Moment->from_string('2015-02-18T10:50:31.521345123+10:00');
    
    # Генерация строки по шаблону
    my $str = $tm->strftime("%Y.%m.%d %H-%M-%S (%f) %z");
    
    # Прибавить 1 год 2 месяца 3 дня 4 часа 5 минут и 6 секунд
    my $tm2 = $tm1->plus_years(1)->plus_months(2)->plus_days(3)
        ->plus_hours(4)->plus_minutes(5)->plus_seconds(6);
    
    # Сравнить даты
    my $result = $tm1 <=> $tm2; # результат: -1
    
    # Вычитание
    my $interval_in_seconds = $tm1->epoch - $tm2->epoch;
    
    # Определение начала / конца недели и месяца
    $tm = $tm->with_hour(0)
        ->with_minute(0)
        ->with_second(0)
        ->with_nanosecond(0)
    my $week_begin  = $tm->with_day_of_week(1);
    my $week_end    = $tm->with_day_of_week(7)
    my $month_begin = $tm->with_day_of_month(1);
    my $month_end   = $tm->with_day_of_month( $tm->length_of_month );
    

    Тесты и результаты


    Код тестов доступен на GitHub.

    Тестовая среда:
    Intel Core i5-2557M CPU @ 1.70GHz, 4Gb, Mac OS X 10.10.2
    Perl 5.20.1 (в Perlbrew)
    Benchmark (1.18)

    В результатах теста для удобства используются сокращённые названия модулей: Date::Manip = D::M, DateTime = DT и т. д.

    Создание объектов с текущим локальным временем

                        Rate   D::M    DT T::P D::C P::D (new time) P::D (new)  T::M P::D (now)
    D::M              3373/s     --  -73% -97% -98%            -99%      -100% -100%      -100%
    DT               12582/s   273%    -- -89% -92%            -98%       -98%  -98%       -99%
    T::P            119244/s  3435%  848%   -- -20%            -81%       -82%  -86%       -86%
    D::C            149116/s  4321% 1085%  25%   --            -77%       -78%  -82%       -82%
    P::D (new time) 644519/s 19009% 5022% 441% 332%              --        -5%  -22%       -23%
    P::D (new)      677138/s 19976% 5282% 468% 354%              5%         --  -18%       -19%
    T::M            830755/s 24531% 6503% 597% 457%             29%        23%    --        -1%
    P::D (now)      839971/s 24804% 6576% 604% 463%             30%        24%    1%         --
    

    Panda::Date — ожидаемо самый быстрый. Но Time::Moment неожиданно почти так же быстр!

    Создание объектов с текущим временем UTC

              Rate   D::M     DT   T::P   D::C   P::D   T::M
    D::M    1999/s     --   -83%   -98%   -99%  -100%  -100%
    DT     11498/s   475%     --   -90%   -93%   -98%   -99%
    T::P  120130/s  5909%   945%     --   -26%   -82%   -92%
    D::C  161964/s  8001%  1309%    35%     --   -76%   -89%
    P::D  671656/s 33495%  5741%   459%   315%     --   -55%
    T::M 1476686/s 73761% 12743%  1129%   812%   120%     --
    

    Все становятся медленнее на этой операции. Все, кроме Time::Moment. Он становится значительно быстрее и выходит на первое место.

    Определение времени в конкретной временной зоне

    Определение смещения по зоне в этом тесте не производим. Смещение задаём заранее +10 часов. Разные модули по-разному предлагают задавать смещение. Одни в секундах, другие в минутах, третьи строкой типа '+1000'.

             Rate   D::M     DT   D::C   T::P   P::D   T::M
    D::M   1725/s     --   -61%   -95%   -97%  -100%  -100%
    DT     4439/s   157%     --   -87%   -92%   -99%   -99%
    D::C  33939/s  1868%   665%     --   -39%   -92%   -95%
    T::P  55584/s  3122%  1152%    64%     --   -87%   -92%
    P::D 438601/s 25327%  9782%  1192%   689%     --   -40%
    T::M 735173/s 42520% 16463%  2066%  1223%    68%     --
    

    Парсинг строки (дата, время и зона)

    Date::Calc умеет парсить только даты (без времени) и только в определённом формате, поэтому в данном тесте он не участвует.

                       Rate    D::M DT (Strptime) DT (ISO8601)    T::P   P::D   T::M
    D::M             1138/s      --          -43%         -63%    -99%  -100%  -100%
    DT (Strptime)    1993/s     75%            --         -36%    -98%  -100%  -100%
    DT (ISO8601)     3090/s    171%           55%           --    -98%  -100%  -100%
    T::P           127471/s  11097%         6297%        4025%      --   -84%   -90%
    P::D           792571/s  69519%        39675%       25547%    522%     --   -37%
    T::M          1266979/s 111190%        63482%       40899%    894%    60%     --
    

    Генерация по шаблону

    Date::Calc не умеет самостоятельно форматировать строки и данный тест тоже пропускает.

             Rate    DT  D::M  T::P  P::D  T::M
    DT    10895/s    --  -55%  -95%  -98%  -98%
    D::M  24273/s  123%    --  -88%  -95%  -95%
    T::P 202159/s 1756%  733%    --  -57%  -59%
    P::D 473339/s 4245% 1850%  134%    --   -3%
    T::M 488258/s 4382% 1912%  142%    3%    --
    

    Вычисление даты (сложение / вычитание)

                      Rate   D::M     DT  T::P  D::C T::M P::D (array) P::D (string)
    D::M            3493/s     --   -21%  -71%  -88% -99%         -99%         -100%
    DT              4403/s    26%     --  -64%  -85% -99%         -99%         -100%
    T::P           12092/s   246%   175%    --  -58% -98%         -98%          -99%
    D::C           29019/s   731%   559%  140%    -- -94%         -95%          -97%
    T::M          487483/s 13854% 10972% 3932% 1580%   --         -16%          -48%
    P::D (array)  579109/s 16477% 13053% 4689% 1896%  19%           --          -38%
    P::D (string) 934644/s 26655% 21129% 7630% 3121%  92%          61%            --
    

    Сравнение дат

    Нужно понимать, что разные модули работают с разной точностью. Time::Moment и DateTime — с точностью до наносекунды (для них разница между датами была 1 наносекунда), остальные до секунды (разница 1 секунда).

              Rate   D::M   D::C     DT   T::P   P::D   T::M
    D::M   27427/s     --   -34%   -64%   -92%   -99%   -99%
    D::C   41837/s    53%     --   -46%   -88%   -99%   -99%
    DT     77067/s   181%    84%     --   -79%   -98%   -98%
    T::P  363376/s  1225%   769%   372%     --   -88%   -89%
    P::D 3145500/s 11369%  7418%  3981%   766%     --    -7%
    T::M 3399073/s 12293%  8025%  4311%   835%     8%     --
    

    Работа с более высокой точностью не мешает Time::Moment и тут занять первое место.

    Определение интервала между датами (вычитание дат)

              Rate     DT   D::M   D::C   T::P   P::D   T::M
    DT      6892/s     --   -42%   -88%   -97%   -99%  -100%
    D::M   11964/s    74%     --   -78%   -95%   -98%   -99%
    D::C   55448/s   704%   363%     --   -75%   -93%   -97%
    T::P  219763/s  3089%  1737%   296%     --   -71%   -89%
    P::D  767510/s 11036%  6315%  1284%   249%     --   -63%
    T::M 2085234/s 30155% 17329%  3661%   849%   172%     --
    

    Time::Moment хоть и работает быстрее всех, но делает наиболее ограниченную операцию — вычитание epoch (впрочем, достаточную во многих случаях). DateTime работает медленнее всех, но единственный, кто работает с точностью до наносекунд (что может быть лишним, но всё же).

    Определение начала и конца недели и месяца от текущей даты

    Комплексный тест, выполняющий сразу несколько операций. В качестве окончания недели / месяца использую начало последнего дня (время 00:00:00).

             Rate    D::M      DT    T::P    D::C    P::D    T::M
    D::M   93.9/s      --    -88%    -99%    -99%   -100%   -100%
    DT      790/s    741%      --    -92%    -96%    -99%   -100%
    T::P  10060/s  10608%   1173%      --    -45%    -93%    -94%
    D::C  18309/s  19388%   2217%     82%      --    -87%    -90%
    P::D 138748/s 147586%  17458%   1279%    658%      --    -22%
    T::M 177777/s 189129%  22397%   1667%    871%     28%      --
    

    Диаграмма по результатам всех тестов

    Там, где значений не видно — они ничтожны (за исключением тех тестов, где модуль совсем не участвует).

    Выводы


    Time::Moment — самый быстрый модуль, и при этом работает с точностью до наносекунд. Позволяет выполнять почти все необходимые операции, кроме парсинга по кастомному шаблону и вычитания дат. Мой личный фаворит по результатам тестов и по удобству интерфейса.

    DateTime — наоборот, один из самых медленных модулей. Но при этом один из самых функциональных и способен работать с высокой точностью.

    Date::Calc — неплохой модуль с простым и понятным интерфейсом. По сравнению с конкурентами никаких преимуществ не имеет.

    Panda::Date — второй по скорости модуль. По сравнению с Time::Moment особыми преимуществами не обладает, а ограничение его (Perl 5.18) может быть критичным.

    Time::Piece — достаточно быстрый core-модуль, но со своими особенностями в математике, которые нужно учитывать.

    Date::Manip — самый медленный модуль с очень специфичным интерфейсом. Главное его преимущество — возможность парсить строки типа «8:00pm December tenth». Если есть необходимость в таком функционале, то, наверное, можно использовать этот модуль, но я бы поискал другие решения под свои задачи.

    К сожалению, главной цели мне добиться не удалось — нет модулей, способных решить все поставленные задачи и работающих достаточно быстро. Но есть явный лидер — Time::Moment. И моя рекомендация будет такая:
    использовать Time::Moment везде, где его функционала достаточно, а недостающий функционал закрывать модулями Time::Piece (благо доступен всегда как core-модуль) или DateTime (в самом крайнем случае).

    Статьи на тему:
    www.perl.com/pub/2003/03/13/datetime.html
    blogs.perl.org/users/chansen/2014/08/timemoment-vs-datetime.html
    perltricks.com/article/148/2015/2/2/Time--Moment-can-save-time
    REG.RU
    69,00
    Каждый день мы делаем Рунет лучше! Вы с нами?
    Поделиться публикацией

    Похожие публикации

    Комментарии 11

      0
      В каких задачах работа с датами требует особого быстродействия?

      Или так — в каких задачах быстродействие важнее универсальности DateTime?
        0
        Какая-то работа с датами, например, есть почти на всех страницах сайта. А где-то её даже много (навскидку — форматирование/локализация дат в каком-нибудь большом списке, например, счетов пользователя или операций). Заменить инструмент и получить прирост по скорости даже пусть, условно, 1%, но в очень большом количестве мест — вроде и пустячок, но приятно.

        А какие есть причины не использовать более быстрый модуль вместо/параллельно DateTime там, где его можно использовать, и он приносит профит? У нас с DateTime свободные отношения и никаких обязательств хранить друг другу верность.
          0
          Ну как…

          Вместо — значит, переписать имеющийся код.

          Параллельно — значит, не единообразно, плюс лишняя зависимость.

          DateTime умеет всё, а TimeMoment нет, придется исхитряться. Вот и возникает вопрос — стоит ли овчинка выделки?
            0

            В моей задаче надо сделать вычисления с датами раз в секунду за интервал в два дня. Вариант с Time::Moment делает это в 70 раз быстрее чем вариант с DateTime. Такая разница заставляет задуматься.
            Причём некоторые операции (парсинг) я делаю с DateTime.

          +1
          Статистика и отчеты, какие-нибудь сервисы работающие под большой нагрузкой (вроде очередей). Да много где еще может возникнуть вопрос о быстродействии (и потреблении памяти кстати тоже). Если проблем со скоростью DateTime нет — то конечно нет смысла переписывать код. Но если проблемы все таки возникли — есть хорошие альтернативы.

          По поводу переписывания кода. У нас есть некоторый набор функций работы со временем. Кода там не много, но используются они часто. Поменять код в них — значит увеличить производительность во многих местах сразу.

          В конечном счета, каждая команда сама решает нужны ли альтернативы и дополнительные зависимости в проекте.
          0
          Спасибо за обзор.

          Конечно, надо использовать лучшие решения, и быстродействие — решающий фактор в конечно итоге.

          Time::Moment использует XS — поэтому работает быстро.
            0
            Спасибо за тест. Пригодился.

            Но написание в Readme хотя бы:
            cpanm Modern::Perl DateTime Date::Manip Time::Piece Panda::Date Date::Calc Time::Moment DateTime::Format::ISO8601
            или приложенный cpanfile сильно сэкономили бы время, и позволило бы прогнать тесты неопытным пользователям(или тем кому лень выяснять, почему они не хотят запускаться просто так)

            P.S.: Кстати, модель процессора стоит указывать точнее. Я проверил тесты на Xeon E3-1240 V2 и i7-3770 — и результат сильно в некоторых случаях отличается в пользу Panda::Time
              0
              Спасибо за замечания. Внес дополнительную информацию в статью и readme к тестам.

              А какие результаты получились у Вас?
            0
            О, спасибо. Я использовал DateTime из-за клевой поддержки таймзон. Но да, он монструозный.
              0
              Не затронут один важный показатель — интервал дат, с которым работают модули. Большинство из них построено на целом числе — то есть секунды с 1970-го года по 2037-й (если не ошибаюсь), так что даже дату ВОВ они представить не могут. DateTime тут чуть лучше, но и тут, как видно, всё зависит от разрядности:

              Internally, dates are represented the number of days before or after 0001-01-01. This is stored as an integer, meaning that the upper and lower bounds are based on your Perl's integer size ($Config{ivsize}).

              The limit on 32-bit systems is around 2^29 days, which gets you to year (±)1,469,903. On a 64-bit system you get 2^62 days, (±)12,626,367,463,883,278 (12.626 quadrillion).

              Как бы то ни было, эта особенность — возможность работать на 64-разрядных машинах с любыми датами — ставит DateTime вне конкуренции.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое