先日、社内にてPHPのリファレンスカウンティングに関する話題が取り上げられたので、リファレンスカウンティングについて少し紹介します。
代入の裏側
PHP(のスクリプティングエンジンであるZend Engine)では、メモリを効率的に利用するため、ある変数を別の変数に代入した際に値の複製をすぐには行わないようになっています。
例えば、以下のコードを実行した場合、
<?php
$a = 'on';
$b = $a;
PHPインタプリタの内部では下図のように、2つの変数がいずれも同一の値を参照するような状態になります。

PHPの内部状態1
次のように変数の値に対して変更を加えると、値の複製が行われて2つの変数が別々の値を指すようになります。
<?php
$a = 'on';
$b = $a;
$b .= 'e';
この時、内部の状態は以下のようになります。

PHPの内部状態2
このような手法は一般的にコピーオンライト(Copy-on-write)またはリファレンスカウンティング(Reference Counting)と呼ばれます(PHPコミュニティでは後者が一般的なようです)。
メモリの節約を考える
さきほどの動きを踏まえて、サイズの大きい変数を扱う場合を考えてみます。
<?php
ini_set('memory_limit', '2M');
function get_1MB_data()
{
return str_pad('', 1024 * 1024, '0');
}
$data1 = get_1MB_data();
$data2 = $data1;
$data3 = $data1;
$data4 = $data1;
$data5 = $data1;
上記のスクリプトは、ini_set()を使ってPHPスクリプトが使用できるメモリ量を2MBに制限してから、1MBの文字列を変数に読み込み、それをさらに4つの変数に代入するという処理になっています。
一見すると、1MB×5個=5MBのメモリが消費されるため、メモリ不足のエラーが起きてしまいそうですが、実際は問題なく動作します。
これは、$data2〜$data5への代入時に値の複製が行われず、実際には1MB分のメモリしか消費されないためです。
ここで、さらに$data5に文字の追加を行って値の複製を発生させてみましょう。さきほどのスクリプトの最終行を以下のように変更します。
$data5 .= 'a';
すると、予想通りメモリ不足のエラーが発生してスクリプトの実行が中断されます。
$ php test.php
PHP Fatal error: Allowed memory size of 2097152 bytes exhausted (tried to allocate 1048577 bytes) in test.php on line 14
Fatal error: Allowed memory size of 2097152 bytes exhausted (tried to allocate 1048577 bytes) in test.php on line 14
このような挙動を知っていると、メソッドの引数としてサイズの大きな変数を渡す場合などに、メモリ節約のために明示的に参照渡しとする必要があるかどうかを「メソッド内で引数の値が変更されていないかどうか」で判断することができるようになります。
参考