PHPによる日付計算の速度比較と2038年問題
PHPで指定した日時の前日あるいは翌日といった日付を取得するには
- strtotime関数で変換する方法
- DateTimeクラスを使う方法
- 時分秒まで分解してmktime関数でシリアル値に戻す方法
等がありますが、それぞれどの方法が一番早いのだろうか、と唐突に気になったので比較してみました。
テスト環境
高性能CPUだと一瞬で終わっちゃうので、テストするWebサーバーにはCeleron J1900を搭載したとっても遅いPCを使用。
手持ちのワットチェッカーで調べたところ、消費電力が平均8W程度しかない驚異の省電力PCです。
2015年に購入して以来、我が家のファイルサーバー兼Webサーバーとして4年以上大活躍してくれていますが、正直そろそろ買い替えたいw
strtotime関数で日付計算する例
サンプルコード
$date = '2038-05-25 00:00:00';
$t1 = microtime(true);
for ($c = 0; $c < 10000; $c++) {
//0.19856405258179sec
$serial = strtotime($date);
$date = date('Y-m-d H:i:s', strtotime('+1 hour', $serial));
echo $date.'<br>';
}
$t2 = microtime(true);
echo $t2 - $t1.'秒';
実行結果
0.19856405258179秒
解説
2038年5月25日を基準として、1時間ずつ加算して表示するサンプルで、1万時間先まで表示しています。
ちなみにhour/hours、day/daysのような単数形/複数形の表記はどちらでも通ります。
具体的には1日加算する場合は+1 dayですが、+1daysと書いてもOKだし、2日加算する場合も+2 daysでも+2 dayでもOK。
地味に親切ですね。
DateTimeクラスで日付計算する例
サンプルコード
$date = '2038-05-25 00:00:00';
$t1 = microtime(true);
for ($c = 0; $c < 10000; $c++) {
//0.21323204040527sec
$dt = new DateTime($date);
$dt->modify('+1 days');
$date = $dt->format('Y-m-d H:i:s');
echo $date.'<br>';
}
$t2 = microtime(true);
echo $t2 - $t1.'秒';
実行結果
0.21323204040527秒
解説
2038年5月25日を基準として、1時間ずつ以下略
strtotimeでは日付文字列をシリアル値に変換しましたが、こちらではDateTimeクラスにするというだけの違いです。
日付の加算/減算はmodifyメソッドを使い、DateTimeクラスから日付文字列への変換にはformatメソッドを使います。
クラスを使う分、若干のオーバーヘッドがあり、strtotimeの例よりはほんのわずかに遅いようです。
ただ、後述しますが、32bitのWebサーバーを使う場合は2038年問題をクリアできる唯一の方法のため、問答無用でDateTimeクラスの使用をおすすめします。
mktime関数で日付計算する例
サンプルコード1
$date = '2038-05-25 00:00:00';
$t1 = microtime(true);
for ($c = 0; $c < 10000; $c++) {
//0.26579284667969sec.
list($y, $m, $d, $h, $i, $s) = sscanf($date, '%04d-%02d-%02d %02d:%02d:%02d');
$date = date("Y-m-d H:i:s", mktime($h + 1, $i, $s, $m, $d, $y));
echo $date.'<br>';
}
$t2 = microtime(true);
echo $t2 - $t1.'秒';
サンプルコード2
$date = '2038-05-25 00:00:00';
$t1 = microtime(true);
for ($c = 0; $c < 10000; $c++) {
//0.12251996994019sec.
$y = substr($date, 0, 4);
$m = substr($date, 5, 2);
$d = substr($date, 8, 2);
$h = substr($date, 11, 2);
$i = substr($date, 14, 2);
$s = substr($date, 17, 2);
$date = date("Y-m-d H:i:s", mktime($h + 1, $i, $s, $m, $d, $y));
echo $date.'<br>';
}
$t2 = microtime(true);
echo $t2 - $t1.'秒';
実行結果
サンプル1: 0.26579284667969秒
サンプル2: 0.12251996994019秒
解説
日付文字列の年/月/日/時/分/秒の切り出しを自前で行い、あとはmktime関数で日付計算をする例ですが、今回は2種類用意しました。
(mktime関数は2019/05/32といったありえない日付を渡しても2019/06/01に変換してくれます)
サンプル1のほうはsscanf関数を使い、書式を '%04d-%02d-%02d %02d:%02d:%02d' と指定することで、見やすさと速度が両立できている…………とぼくは思い込んで長年使っていました…(;´Д`)
しかし実際は結果を見てのとおり、どの方法よりもおっそいですね…。sscanfってこんなに遅かったのか…。
strtotime関数、DateTimeクラス、mktime関数の速度をそれぞれ調べるつもりが全然別のsscanfがボトルネックになっていることがわかってしまいました。
そして、それを改善すべく作ったサンプル2がちょっぱや。愚直にsubstr関数で年/月/日/時/分/秒を切り出すという泥臭いやり方で見た目もよくありませんが、変換速度で言えば最速でした。
2038年問題について
PHPの日付関連の関数でよく使われるUNIXタイムスタンプ(シリアル値)には1970年1月1日からの経過秒数が使われています。
つまり、↓この実行結果(UTC時間の1970年1月1日のシリアル値を求める)はゼロになります。
date_default_timezone_set('UTC');
echo mktime(0,0,0,1,1,1970);
そして、問題はこのシリアル値がint型であるということ。
int型の最大値はOSに依存しており、32ビットOSなら2,147,483,647、64ビットOSなら9,223,372,036,854,775,807まで扱えます。
桁がいっぱいあってワケわからんですよね。
32ビットOSならおよそ21億が最大値、と覚えておけば良いと思います。
1970年1月1日から約21億秒後が2038年1月19日3時14分7秒(UTC)であり、32ビットOSではこれ以上先の日付は扱えない、ということになります。扱おうとするとオーバーフローして、1970年から再カウントします。
ただ、32ビットOSだとすべての数値の限界が21億になる、というわけではないのです。
だってそうでしょう。そんな限界値があったら21億円を超えるお金の計算とか出来ないじゃないですか。そんなお金見たこともないですけどw でも、業務系のシステムだったら、(別に銀行に限らずとも)普通に扱えないといけない数字です。
つまり、まぁこんだけあれば十分やろ、とint型で設計した日付関連の関数がよくないのですが、今更そっちを変更するのも難しいようです。
(一応、1970年からしか扱えないのは不便なので、負の数値に対応することで1901年からの日付が扱えるようにはなったらしい)
古いシステムへの影響が大きそうなので既存のstrtotimeやmktimeの仕様は変更できないけれど、新しい日付関数なら、ってことでPHP5.2以降に導入されたDateTimeクラスのほうは2038年問題をクリアしています。
というわけで、2038年問題についてざっくりまとめるとこんな感じ。
- strtotimeやmkktime等のUNIXタイムスタンプを使う関数を32ビットOS上で使う場合は2038年問題が発生する。
- DateTimeクラスは32ビットOSでも2038年問題は発生しない。
- 64ビットOSを使う限りは従来の日付関数を使っていても問題ない。
32ビットのサーバーを使う可能性があるのなら、おとなしくDateTimeクラスを使いましょうってことですね。
まとめ
- 日付文字列の加算/減算などの計算をする場合、もっとも早いのはsubstrで年/月/日/時/分/秒を切り出してmktime関数で計算する方法。
- 次に早いのはstrtotime関数を使った方法。
- その次に早いのはDateTimeクラスを使った方法。
- 但し、32ビットOSを使う可能性がある場合は2038年問題が発生する可能性があるためDateTimeクラスを使うこと。
といったところでしょうか。
今回なんとなくでテストしただけですが、sscanfが想像以上におっそいことがわかったのが一番の収穫だったかも知れません。