この記事は, Perl Advent Calendar 2023の6日目の記事です. 12月6日の26時くらいに公開されているかもしれませんが気にしないでください.
5日目の記事は, @shogo82148 さんの「Amazon Linux 2023ベースのAWS Lambda Perl Runtimeを公開しました」でした.
タイトルにある通りで, MySQLのDATETIME(例えば2023-12-07 12:34:56
)を, PerlのDateTimeオブジェクトにしたい訳です. まあ, 素早さ最優先ならTime::Momentを使いましょう, で完結するわけですが...
...ただまあ, 様々な事情でどうしてもDateTimeを使いたい, という場合もあるでしょう. そういう時にどういう選択肢が取りうるか? というのを考えたのでご紹介します. もし, もっと良い作戦があったら教えてください.
王道
まず王道? の選択肢は, DateTime::Format::MySQLを使うことでしょう. MySQLのDATE, DATETIME, TIME, TIMESTAMPをパースして, DateTimeのオブジェクトを返してくれる君です.
別解
Time::StrptimeのDESCRIPTIONを見ると, 日時のパースで一番速いのはやはりTime::Momentのようです.
そこで, 「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%くらい高速化できるので, やってみる価値はありそうですね.