Masteries

技術的なことや仕事に関することを書いていきます.

MySQLのDATETIMEをとにかく素早くPerlのDateTimeオブジェクトにしたい

この記事は, Perl Advent Calendar 2023の6日目の記事です. 12月6日の26時くらいに公開されているかもしれませんが気にしないでください.

qiita.com

5日目の記事は, @shogo82148 さんの「Amazon Linux 2023ベースのAWS Lambda Perl Runtimeを公開しました」でした.

shogo82148.github.io


タイトルにある通りで, MySQLのDATETIME(例えば2023-12-07 12:34:56)を, PerlのDateTimeオブジェクトにしたい訳です. まあ, 素早さ最優先ならTime::Momentを使いましょう, で完結するわけですが...

metacpan.org

...ただまあ, 様々な事情でどうしてもDateTimeを使いたい, という場合もあるでしょう. そういう時にどういう選択肢が取りうるか? というのを考えたのでご紹介します. もし, もっと良い作戦があったら教えてください.

王道

まず王道? の選択肢は, DateTime::Format::MySQLを使うことでしょう. MySQLのDATE, DATETIME, TIME, TIMESTAMPをパースして, DateTimeのオブジェクトを返してくれる君です.

metacpan.org

別解

Time::StrptimeのDESCRIPTIONを見ると, 日時のパースで一番速いのはやはりTime::Momentのようです.

metacpan.org

そこで, 「DATETIMEのparseだけTime::Momentを使って, その結果をDateTimeにする」という選択肢を思いつきました. 但し, Time::Momentのfrom_stringは, そのままMySQLのDATETIMEをパースできないので,

my $str = '2023-12-07 12:34:56';

$str .= 'Z' if substr($str, -1, 1) ne 'Z';
substr($str, 10, 1, 'T') if substr($str, 10, 1) eq ' ';

my $tm = Time::Moment->from_string($str);

...みたいな手当が必要になりますが. 後は, Time::Momentのオブジェクト(にある日時の情報)を使ってDateTimeのオブジェクトをnewしてあげれば良いわけです.

推測するな計測せよ

具体的にどれくらい素早くなるのでしょうか? 計測してみましょう. 以下の4つをBenchmarkを使って比較しました:

  • DateTime::Format::MySQL
  • Time::Moment + DateTime#new
  • Time::Moment + DateTime#from_epoch
  • Time::Moment (比較用)

DateTime::Format::MySQLが一番遅くて, Time::Momentが一番速いのはまあ確定として, Time::Momentでパースした結果からはDateTime#newを使う選択肢と, DateTime#from_epochを使う選択肢があります. なんとなくですが, 後者の方がepochからyearやhourなどを求める処理が必要なので, 誤差レベルで遅そうなイメージがありますが... 果たして.

use strict;
use warnings;

use Benchmark qw(timethese);
use DDP;

use DateTime::Format::MySQL;
use DateTime;
use Time::Moment;

my $str = '2023-12-07 12:34:56';

sub datetime {
    my ($str) = @_;
    return DateTime::Format::MySQL->parse_datetime($str);
}

sub time_moment_and_from_epoch {
    my ($str) = @_;
    $str .= 'Z' if substr($str, -1, 1) ne 'Z';
    substr($str, 10, 1, 'T') if substr($str, 10, 1) eq ' ';

    my $tm = Time::Moment->from_string($str);
    return DateTime->from_epoch($tm->epoch);
}

sub time_moment_and_new {
    my ($str) = @_;
    $str .= 'Z' if substr($str, -1, 1) ne 'Z';
    substr($str, 10, 1, 'T') if substr($str, 10, 1) eq ' ';

    my $tm = Time::Moment->from_string($str);
    return DateTime->new(
        year       => $tm->year,
        month      => $tm->month,
        day        => $tm->day_of_month,
        hour       => $tm->hour,
        minute     => $tm->minute,
        second     => $tm->second,
    );
}

sub time_moment {
    my ($str) = @_;
    $str .= 'Z' if substr($str, -1, 1) ne 'Z';
    substr($str, 10, 1, 'T') if substr($str, 10, 1) eq ' ';

    return Time::Moment->from_string($str);
}

# パースした結果, 同じ epoch を指していることを確認している
p datetime($str)->epoch;
p time_moment_and_from_epoch($str)->epoch;
p time_moment_and_new($str)->epoch;
p time_moment($str)->epoch;

timethese(2_000_000, {
    'datetime'                   => sub { my $obj = datetime($str) },
    'time_moment_and_from_epoch' => sub { my $obj = time_moment_and_from_epoch($str) },
    'time_moment_and_new'        => sub { my $obj = time_moment_and_new($str) },
    'time_moment'                => sub { my $obj = time_moment($str) },
});

以下は実行結果です:

$ perl datetime.pl
1701952496
1701952496
1701952496
1701952496
Benchmark: timing 2000000 iterations of datetime, time_moment, time_moment_and_from_epoch, time_moment_and_new...
  datetime: 35 wallclock secs (35.27 usr +  0.23 sys = 35.50 CPU) @ 56338.03/s (n=2000000)
time_moment:  1 wallclock secs ( 0.62 usr +  0.00 sys =  0.62 CPU) @ 3225806.45/s (n=2000000)
time_moment_and_from_epoch: 18 wallclock secs (17.86 usr +  0.12 sys = 17.98 CPU) @ 111234.71/s (n=2000000)
time_moment_and_new: 25 wallclock secs (24.52 usr +  0.07 sys = 24.59 CPU) @ 81333.88/s (n=2000000)

というわけで, 結果は速い順に Time::Moment > Time::Moment + DateTime#from_epoch > Time::Moment + DateTime#new > DateTime::Format::MySQL, でした. DateTime#newよりDateTime#from_epochの方が素早いんですね. というかこうやって見るとTime::Momentが早すぎる...

高速化という意味では, やはりTime::Moment化するのが理想です. とはいえそうもいかない場合はTime::MomentでパースしてDateTime#from_epochする... という手でも, ベンチマークの結果を見る限り50%くらい高速化できるので, やってみる価値はありそうですね.