コピーオンライトと参照渡しを理解する
コピーオンライトとは、変数を代入した時点では実体を共有したままにし、どちらかが書き換えられた瞬間に初めて複製を作る仕組みです。読むだけの配列の受け渡しが軽くなるPHP内部の最適化です。
代入してもすぐにはコピーされない
PHPで大きな配列を別の変数に代入すると、まるごとコピーされて重いのでは、と心配になりますが、実際はそうではありません。内部にはコピーオンライト、略してCOWという仕組みがあって、代入した時点では実体を共有したままにしておき、どちらかが書き換えられた瞬間に初めて複製が作られます。だから読むだけなら何度渡してもコストはほとんどかからないわけですね。
$big = range(1, 1000000);
$copy = $big; // ここではまだ複製されない。実体を共有している
// $copy を書き換えた瞬間に初めてコピーが発生する
$copy[0] = 999; // ここで実体が分離する
この性質のおかげで、関数に配列を値渡ししても、中で変更しなければ余計なメモリは食いません。「値渡しは遅い」と一律に思い込む必要はない、というのが実感ですね。
関数に渡しても読むだけなら軽い
大きな配列を関数に渡して集計だけする、といった処理は、値渡しのままで十分に軽いです。COWが効いているので、関数内で書き換えない限りコピーは走りません。むしろ変に & を付けて参照渡しにすると、意図が読みにくくなったり、呼び出し元の値をうっかり壊したりする副作用のほうが怖いと思います。
function sumAll(array $items): int
{
$total = 0;
foreach ($items as $v) { // 読むだけ。コピーは発生しない
$total += $v;
}
return $total;
}
sumAll($big); // 巨大配列でも余分な複製は起きない
&の参照渡しは慎重に
参照渡しがまったく不要というわけではありませんが、性能目的で安易に付けるのは避けたいですね。特に foreach で $value を参照にした後、変数を使い回すと最後の要素が書き換わる、という有名な落とし穴があります。参照はループを抜けたら unset するのが安全策です。参照は「呼び出し元の変数を本当に書き換えたいとき」だけに絞り、性能はCOW任せにするのが結局いちばん素直だと思います。
$arr = [1, 2, 3];
foreach ($arr as &$v) {
$v *= 2;
}
unset($v); // これを忘れると後のバグの温床になる
// 以降でうっかり $arr を使うと最後の要素が壊れがち
まとめ
PHPの配列はコピーオンライトで管理されるので、読むだけの受け渡しは思っているより軽いです。値渡しを恐れて & を多用すると、副作用や foreach の参照残りといった別の問題を招きがちですね。性能はCOWに任せ、参照渡しは呼び出し元を本当に書き換えたいときだけに限る。この線引きにしておくと、速さと安全さの両方が取りやすい気がします。
よくある質問
Q. 大きな配列を値渡しすると遅くなりますか?
A. 関数内で書き換えない限りコピーオンライトが効くので、値渡しでもほとんどコストはかかりません。読むだけなら値渡しを恐れる必要はないと思います。
Q. 性能のために参照渡しにすべきですか?
A. 性能目的で & を安易に付けるのは避けたいところです。呼び出し元を本当に書き換えたいときだけに絞り、速度はコピーオンライトに任せるのが素直だと思いますね。