はじめに
一覧画面の表示速度を調査している中で、行ごとに日時オブジェクトを新規生成している処理を見つけました。
そこから、そもそも DateTime と DateTimeImmutable はどちらが速いのか、PHPのバージョンが変わると速度はどう変わるのかが気になり、ベンチマークで検証しました。この記事では、その検証結果を扱います。
環境: PHP 7.0 / PHP 8.5
modify()を呼ぶかどうかで2ケースに分ける
DateTime と DateTimeImmutable の違いは、DateTime は自分自身を書き換えるミュータブルなクラスで、DateTimeImmutable は変更のたびに新しいインスタンスを返すイミュータブルなクラスです。
この違いが具体的にどう挙動を変えるかは、DateTimeクラスとDateTimeImmutableクラスの違いを理解するで分かりやすく実例付きで解説されています。
この違いがパフォーマンスにどう影響するかを見るため、入力文字列 2026-08-08 08:00:00 に対して、
- そのまま出力
- 1ヶ月後
- 1ヶ月前
の3ケースを用意して比較を検討しました。
「そのまま出力」は modify() を呼ばずオブジェクト生成のコストだけを見るケースで、残り2つは modify() を呼んで変更のコストも含めたケースです。
検証方法
PHP 7.0とPHP 8.5、それぞれで同じベンチマークスクリプトを実行し、所要時間を計測しました。
入力文字列は2026-08-08 08:00:00で固定
const INPUT_DATE_STRING = '2026-08-08 08:00:00';
DateTime 版の計測関数
指定回数ループしながらオブジェクトを生成し、必要なら modify() で変更し、format() で文字列に戻します。
/**
* DateTime(ミュータブル)で同じ処理を$iterationCount回繰り返し、所要時間を計測する。
*
* @param int $iterationCount 繰り返し回数
* @param string $relativeModifier DateTime::modify()に渡す相対指定。空文字なら変更しない
* @return float 所要時間(ミリ秒)
*/
function benchmarkDateTime($iterationCount, $relativeModifier)
{
$startTime = microtime(true);
for ($i = 0; $i < $iterationCount; $i++) {
$dateTime = new DateTime(INPUT_DATE_STRING);
if ($relativeModifier !== '') {
// DateTimeはミュータブル: modify()は$dateTime自身を書き換える(戻り値を受け直す必要はない)
$dateTime->modify($relativeModifier);
}
$dateTime->format('Y-m-d H:i:s');
}
return (microtime(true) - $startTime) * 1000;
}
DateTimeImmutable 版の計測関数
modify() の戻り値を変数に受け直す点だけが異なります。
/**
* DateTimeImmutable(イミュータブル)で同じ処理を$iterationCount回繰り返し、所要時間を計測する。
*
* @param int $iterationCount 繰り返し回数
* @param string $relativeModifier DateTimeImmutable::modify()に渡す相対指定。空文字なら変更しない
* @return float 所要時間(ミリ秒)
*/
function benchmarkDateTimeImmutable($iterationCount, $relativeModifier)
{
$startTime = microtime(true);
for ($i = 0; $i < $iterationCount; $i++) {
$dateTime = new DateTimeImmutable(INPUT_DATE_STRING);
if ($relativeModifier !== '') {
// DateTimeImmutableはイミュータブル: modify()は元を変えず新しいインスタンスを返すため、必ず受け直す
$dateTime = $dateTime->modify($relativeModifier);
}
$dateTime->format('Y-m-d H:i:s');
}
return (microtime(true) - $startTime) * 1000;
}
3ケースそれぞれに対して呼び出す関数
この2つを、「そのまま出力」「1ヶ月後」「1ヶ月前」の3ケースそれぞれに対して呼び出す関数が runCase() です。
/**
* 1つの日時操作ケース(そのまま/1ヶ月後/1ヶ月前)について、3方式すべてを計測して出力する。
*
* @param string $caseLabel 出力見出しに使うケース名
* @param string $relativeModifier 各benchmarkXxx()に渡す相対指定。空文字なら「そのまま出力」ケースになる
* @param int $iterationCount 繰り返し回数
*/
function runCase($caseLabel, $relativeModifier, $iterationCount)
{
echo "=== {$caseLabel} (n={$iterationCount}) ===\n";
printBenchmarkResult('strtotime()+date()', benchmarkStrtotime($iterationCount, $relativeModifier));
printBenchmarkResult('DateTime', benchmarkDateTime($iterationCount, $relativeModifier));
printBenchmarkResult('DateTimeImmutable', benchmarkDateTimeImmutable($iterationCount, $relativeModifier));
echo "\n";
}
3ケースをまとめて実行する関数
runAllCases() で3ケースをまとめて実行し、ループ回数1万回と10万回それぞれで3回ずつ計測しました。
/**
* 3つのケース(そのまま出力/1ヶ月後/1ヶ月前)を指定回数で計測する。
*
* @param int $iterationCount 繰り返し回数
*/
function runAllCases($iterationCount)
{
runCase('そのまま出力', '', $iterationCount);
runCase('1ヶ月後', '+1 month', $iterationCount);
runCase('1ヶ月前', '-1 month', $iterationCount);
}
echo 'PHP ' . PHP_VERSION . "\n";
echo '入力: ' . INPUT_DATE_STRING . "\n\n";
runAllCases(10000);
runAllCases(100000);
計測結果
そのままの出力:ほぼ差がない
ループ10万回、3回計測の平均です。
| PHP | DateTime | DateTimeImmutable | DateTimeが速い割合 |
|---|---|---|---|
| 7.0 | 111.6ms | 112.2ms | 0.5% |
| 8.5 | 89.2ms | 89.6ms | 0.4% |
両バージョンとも1%未満で、実質差がありません。
→modify() を呼ばないため、ほとんど差はありませんでした。
「1ヶ月後」「1ヶ月前」:DateTimeが一貫して速い
同じ条件で「1ヶ月後」「1ヶ月前」を計測すると、結果が変わります。
| PHP | ケース | DateTime | DateTimeImmutable | DateTimeが速い割合 |
|---|---|---|---|---|
| 7.0 | 1ヶ月後 | 163.6ms | 173.5ms | 5.7% |
| 7.0 | 1ヶ月前 | 161.4ms | 175.2ms | 7.9% |
| 8.5 | 1ヶ月後 | 144.9ms | 156.9ms | 7.6% |
| 8.5 | 1ヶ月前 | 139.7ms | 151.3ms | 7.7% |
4パターンとも DateTime が速く、平均すると約7%の差になります。
理由は推測ですが、DateTimeImmutable は modify() を呼ぶたびに新しいインスタンスを生成するのに対し、
DateTime は既存のインスタンスをそのまま書き換えるため、オブジェクトの生成回数そのものが1回少なくなることが影響していると考えられます。
余談
strtotime()+date()も測ってみた
同じベンチマークで、オブジェクトを使わない strtotime() + date() も測りました。
/**
* strtotime()+date() で「文字列 → タイムスタンプ → 文字列」を$iterationCount回繰り返し、所要時間を計測する。
* オブジェクトを一切生成しない、最も低レベルな方式。
*
* @param int $iterationCount 繰り返し回数
* @param string $relativeModifier strtotime()の第一引数に渡す相対指定(例: '+1 month')。空文字なら変更しない
* @return float 所要時間(ミリ秒)
*/
function benchmarkStrtotime($iterationCount, $relativeModifier)
{
$startTime = microtime(true);
for ($i = 0; $i < $iterationCount; $i++) {
$timestamp = strtotime(INPUT_DATE_STRING);
if ($relativeModifier !== '') {
// 第二引数に基準タイムスタンプを渡すことで「その時刻からの相対指定」として解釈させる
$timestamp = strtotime($relativeModifier, $timestamp);
}
date('Y-m-d H:i:s', $timestamp);
}
return (microtime(true) - $startTime) * 1000;
}
DateTime と比べると、次のようになりました(ループ10万回、3回計測の平均)。
| PHP | ケース | DateTime | DateTimeImmutable | strtotime()+date() |
|---|---|---|---|---|
| 7.0 | そのまま出力 | 111.6ms | 112.2ms | 102.3ms |
| 7.0 | 1ヶ月後 | 163.6ms | 173.5ms | 164.0ms |
| 7.0 | 1ヶ月前 | 161.4ms | 175.2ms | 163.9ms |
| 8.5 | そのまま出力 | 89.2ms | 89.6ms | 82.1ms |
| 8.5 | 1ヶ月後 | 144.9ms | 156.9ms | 140.1ms |
| 8.5 | 1ヶ月前 | 139.7ms | 151.3ms | 140.0ms |
PHP 7.0・8.5では、strtotime() + date() が3方式の中で最速か、DateTime と僅差の
最速でした。
オブジェクトを一切作らない分速い、という結果です。
PHP 5.6でも計測してみた
PHP 5.6でも同じスクリプトを動かしたところ、ループ10万回・3回計測の平均は次のようになりました。
| PHP | ケース | DateTime | DateTimeImmutable | strtotime()+date() |
|---|---|---|---|---|
| 5.6 | そのまま出力 | 156.6ms | 155.3ms | 150.4ms |
| 5.6 | 1ヶ月後 | 235.9ms | 258.3ms | 490.5ms |
| 5.6 | 1ヶ月前 | 231.8ms | 247.2ms | 493.1ms |
PHP 7.0・8.5では最速だった strtotime() + date() が、
PHP 5.6の「1ヶ月後」「1か月前」では DateTime の約2倍遅くなっています。
→PHP 5.6から7.0にかけて、DateTime と DateTimeImmutable クラスの modify() が大きく最適化された可能性があります。
まとめ
- 生成するだけなら
DateTimeとDateTimeImmutableに差はほぼありませんでした。 - 変更のたびに新しいインスタンスを作る
DateTimeImmutableの場合、modify()を使うメソッドを実行すると、DateTimeと比較して約7%程度遅くなる結果となりました。 strtotime()+date()がDateTimeとDateTimeImmutableよりも速い結果となりました。- しかし、PHP 5.6では逆に
strtotime()+date()がDateTimeとDateTimeImmutableよりも遅い結果となりました。
→PHP 5.6から7.0にかけて、このDateTimeおよびDateTimeImmutableクラスのmodify()が大きく最適化された可能性があります。