あまりにマニアックな情報
2022/04/10 ソース見たので表現修正(内容は同じ)
まとめ
- 文字列追記は結合演算子(.)より結合代入演算子(.=)の方が圧倒的に早い
- つまり
$a = $a . $b
より$a .= $b
が早い
- つまり
- 文字列が大きいほど、結合回数が多いほど顕著に差が出る
- Webアプリならいずれも小さいので、ほとんどのケースでは気にしなくて良い
- エクスポートなど大量のデータを吐き出す処理では顕著に影響する(簡単に何百倍、分単位で差が出る)
- PHPはちゃんと最適化されてる
結合演算子(.)より結合代入演算子(.=)の違い
ほとんどの場合は結合演算子($a = $a . $b
)と結合代入演算子($a .= $b
)は同じで、単にショートハンド位の認識をされていると思う。マニュアルでも特に違いには触れていない。
というか自分がそう思っていたが、想定以上に重いコードがあり、その調査で内部的には大きく違うことに気づいた。
例としてメール等のデータエクスポートを想定し、合計10MBの文字列をN回に分けて結合した際の時間を以下に示す(100, 1000, 10000回; foreach自体の実行は控除済み)。余りに差が出るので対数スケールでも示す(右側)。
結合演算子(.)ではたった10MB, 1万ループで40秒もかかるが、結合代入演算子(.=)では30msと高速。また回数の増加による処理時間増加も結合演算子は大きい。
原因
コードを読んだわけではないので以下は推測。 2022/04/10 ソース見たので表現修正
結合演算子($a = $a . $b
)での処理は、書いてあるまま、$a
と$b
を結合した別のデータを作り、それを改めて$a
としている。つまり、メモリの新規確保(拡張ではなく)と全メモリコピーが都度発生している。そのため容量、回数で処理時間も大きく増加する。
また文字列結合が元変数に追記の形か否かは見ておらず、$a = $a . $b
でも$a = $b . $c
でも同じ処理。
一方結合代入演算子($a .= $b
)では、元変数への追記として最適化されており、元変数(内部のzend_string)のメモリサイズを拡張、追加データだけを末尾にコピーという処理になっている。
考えてみれば当たり前の処理ではあるものの、PHPはちゃんと最適化してるんだなと。
ちなみに文字列結合としてimplode()もあるが、最初に配列をなめて結合後のサイズを確定、一気にメモリ確保・データコピーを行っていた。
配列で扱うオーバーヘッドもあるものの、ベンチしてくださった結果からメモリ使用ちょっと増えるのが許せば柔軟性高くなり、これも良さそう。
検証コード
以下コードでPHP 7.4で確認。
<?php
declare(strict_types=1);
//第一引数に文字列の最終サイズ(バイト)、第二引数に結合回数を指定
$tota_size = (int)$argv[1];
$loop_count = (int)$argv[2];
$chunk_size = (int)($tota_size / $loop_count);
//結合ごとに追記する文字列
$chunk = str_repeat('0', $chunk_size);
//予めループだけで食う時間を計測しておく
//コンパイラ最適化等はないので、空ループでも特に消されることはない
$start = microtime(true);
for ($i = 0; $i < $loop_count; $i++) {
}
$tare = (microtime(true) - $start);
//文字列追記の時間計測
$start = microtime(true);
$memory_init = memory_get_usage();
$str = '';
for ($i = 0; $i < $loop_count; $i++) {
$str .= $chunk; //← ここの処理差し替えで時間比較
//$str = $str . $chunk;
}
$took = (microtime(true) - $start) - $tare;
$memory_used = memory_get_usage() - $memory_init;
echo $took * 1e3; //ms単位