眠すぎて明日が見えない

我が人生、眠さに勝るもの無し

DateTimeオブジェクトそのまま比較するのと、DateTime->epochで比較するのどっちが速いか

ふと気になったのでベンチマーク取ってみた。

ベンチマーク

#!/usr/bin/perl

use strict;
use utf8;
use warnings;

use DateTime;
use Benchmark qw(:all) ;

my $now_1 = DateTime->now;
my $now_2 = $now_1->clone;
my @times = ($now_1, $now_2);
my $today = DateTime->today;

cmpthese(0, {
    'datetime' => sub {
        [grep { $_ >= $today } @times];
    },
    'epoch' => sub {
        [grep { $_->epoch >= $today->epoch } @times];
    },
});

1;
MacoTasu% perl -Ilib datetime-vs-epoch.pl
             Rate datetime    epoch
datetime  32462/s       --     -84%
epoch    205263/s     532%       --

epoch is faster than datetime !

なんとなく予想はできていたけど、epochのほうがやはり速かった。 性能差約5倍かな。

比較の方法

そもそもDateTimeオブジェクトとかDateTime->epochの比較とかどうなってるんだってばよ ってなったのでちょっとだけコードみた。

DateTime->epoch

epochの実装をみると

package DateTime;

...

sub epoch {
    my $self = shift;

    return $self->{utc_c}{epoch}
        if exists $self->{utc_c}{epoch};

    return $self->{utc_c}{epoch}
        = ( $self->{utc_rd_days} - 719163 ) * SECONDS_PER_DAY
        + $self->{utc_rd_secs};
}

のようなになっていて、utc_rd_*などに日にちと秒数が1970年1月1日までの経過日数を引いてそこに1日分の秒数をかけて秒に変換している。 単純に数字の変換の処理をして、その後比較してるだけなので速い。

Datetimeオブジェクトそのまま

Datetimeオブジェクト同士の比較は、闇っぽくてoperatorをoverloadしている。 ゆるふわ脳なので、なんかPerlがよしなに比較してくれてるんだろと思ったけど、頑張ってたのはDateTimeのモジュールでした。

比較しているところは

package DateTime;

...

sub _compare_overload {

    # note: $_[1]->compare( $_[0] ) is an error when $_[1] is not a
    # DateTime (such as the INFINITY value)

    return undef unless defined $_[1];

    return $_[2] ? -$_[0]->compare( $_[1] ) : $_[0]->compare( $_[1] );
}

sub _compare {
    my ( $class, $dt1, $dt2, $consistent ) = ref $_[0] ? ( undef, @_ ) : @_;

    return undef unless defined $dt2;

    if ( !ref $dt2 && ( $dt2 == INFINITY || $dt2 == NEG_INFINITY ) ) {
        return $dt1->{utc_rd_days} <=> $dt2;
    }

    unless ( DateTime::Helpers::can( $dt1, 'utc_rd_values' )
        && DateTime::Helpers::can( $dt2, 'utc_rd_values' ) ) {
        my $dt1_string = overload::StrVal($dt1);
        my $dt2_string = overload::StrVal($dt2);

        Carp::croak( 'A DateTime object can only be compared to'
                . " another DateTime object ($dt1_string, $dt2_string)." );
    }

    if (   !$consistent
        && DateTime::Helpers::can( $dt1, 'time_zone' )
        && DateTime::Helpers::can( $dt2, 'time_zone' ) ) {
        my $is_floating1 = $dt1->time_zone->is_floating;
        my $is_floating2 = $dt2->time_zone->is_floating;

        if ( $is_floating1 && !$is_floating2 ) {
            $dt1 = $dt1->clone->set_time_zone( $dt2->time_zone );
        }
        elsif ( $is_floating2 && !$is_floating1 ) {
            $dt2 = $dt2->clone->set_time_zone( $dt1->time_zone );
        }
    }

    my @dt1_components = $dt1->utc_rd_values;
    my @dt2_components = $dt2->utc_rd_values;

    foreach my $i ( 0 .. 2 ) {
        return $dt1_components[$i] <=> $dt2_components[$i]
            if $dt1_components[$i] != $dt2_components[$i];
    }

    return 0;
}

こんな感じの実装になっていたところまで確認したけど、不思議なことに途中からの記憶が無い。

結果

DateTimeオブジェクトを使った比較をする時は、DateTime->epochで比較したほうが速いのでいいのかもしれないですね。

参考資料

Dave Rolsky / DateTime-1.20 - search.cpan.org

追記(9/1)

とのこと疑問点をいっちー先生があげいてたのでキャッシュっぽいところコメントアウトして回してみたところ

MacoTasu% perl -Ilib datetime-vs-epoch.pl
             Rate datetime    epoch
datetime  34357/s       --     -81%
epoch    178069/s     418%       --

以上の結果になりました。ちょっとだけスコア落ちたけどそれでも約4倍ぐらいはでました。