foreachの参照渡しと罠

foreach。
PHPでは単純な、forやwhileよりもむしろ出番は多いのではないでしょうか。

頻発するコードを高速化することで、アプリケーション全体の高速化が期待できます。

ところで、みなさんはforeach構文で、下記のように記述することにより、参照渡しができることはご存知でしょうか?
サンプル1:

<?php
foreach ($sample_array as $key => &$value) {
    // 何か処理
}
?> 



PHPの変数はコピーオンライト(書き換え時に書き換え用のメモリ領域を確保する)ですのでただ、変数を参照するだけなら、参照渡しにしても処理速度は変わりません。
  サンプル2:

<?php
$start_t = microtime(true);
$range_arr = range(0, 1000000);
foreach ($range_arr as $key => &$value) {
    
}
unset($value);
 echo microtime(true) - $start_t;
?> 

0.17533493041992

サンプル3:

<?php

$start_t = microtime(true);
$range_arr = range(0, 1000000);
foreach ($range_arr as $key => $value) {
    
}
unset($value);
 echo microtime(true) - $start_t;
?> 

0.17597484588623



しかしながら、foreach内で配列の中身を書き換えるような処理の場合は、差が生まれます。

サンプル4:

<?php
$start_t = 
microtime(true);
$range_arr  = range(0, 1000000);
foreach ($range_arr as $key => &$value) {
    $value = 1;
}
unset($value);
echo microtime(true) - $start_t;
?>

0.268750190734806 
 

サンプル5:

<?php
$start_t = microtime(true);
$range_arr = range(0, 1000000);
foreach ($range_arr as $key => $value) {
    $range_arr [$key] = 1;
}
unset($value);
echo microtime(true) - $start_t;
?> 

0.493412017822279



いかがでしょうか?単純な書き換えですが、倍近くの性能差が生まれました。
サンプル5では、配列の値を書き換える際に配列の辞書アクセスが発生するため、このような結果になります。
PHPのメモリ管理や、メモリの辞書に関しての言及はまた後日行うとして、今回はこの結果だけの紹介とさせて頂きます。

しかしながら、このforeachの参照渡し。
幾つかの罠がありますので、その罠について言及したいと思います。
foreach参照の罠その1です。
foreachで参照渡しを使用したあと、参照された変数をunsetしないと下記のような結果になります。

<?php
$sample_arr = array(1,2,3,4,5,6,7,8,9,10);

var_dump($sample_arr);
foreach ($sample_arr as $key => &$value) {
var_dump($value);
}
var_dump($sample_arr);
var_dump($sample_arr[9]);
foreach ($sample_arr as $key => $value) {
var_dump($sample_arr[9]);
}

var_dump($sample_arr);
?>

array(10) {
[0]=>
int(1)
[1]=>
int(2)
[2]=>
int(3)
[3]=>
int(4)
[4]=>
int(5)
[5]=>
int(6)
[6]=>
int(7)
[7]=>
int(8)
[8]=>
int(9)
[9]=>
int(10)
}
int(1)
int(2)
int(3)
int(4)
int(5)
int(6)
int(7)
int(8)
int(9)
int(10)
array(10) {
[0]=>
int(1)
[1]=>
int(2)
[2]=>
int(3)
[3]=>
int(4)
[4]=>
int(5)
[5]=>
int(6)
[6]=>
int(7)
[7]=>
int(8)
[8]=>
int(9)
[9]=>
&int(10)
}
int(10)
int(1)
int(2)
int(3)
int(4)
int(5)
int(6)
int(7)
int(8)
int(9)
int(9)
array(10) {
[0]=>
int(1)
[1]=>
int(2)
[2]=>
int(3)
[3]=>
int(4)
[4]=>
int(5)
[5]=>
int(6)
[6]=>
int(7)
[7]=>
int(8)
[8]=>
int(9)
[9]=>
&int(9)
}

多くの場合、これは意図しない結果ではないでしょうか?
PHPには、ブロックの概念がないために起こる現象ですが、foreacで使用した変数はunsetする癖をつけることで容易に回避できるバグですので、忘れないようにしましょう。
(本件は、PHPのマニュアルにも記載があります。親切な書き方ではありませんが。。。)

foreach参照の罠その2です。
foreachで意図せず高負荷で、実行時間が高くなるコードを書いてしまう場合があります。
ちなみにこれは、foreachの参照渡しを利用している場合のみ起こる不具合です。

早速、具体的な例を見てみましょう!
下記のサンプルコードを見てください。
サンプル1:

<?php
$start_t = microtime(true);
$range_arr = range(0, 10000);
foreach ($range_arr as $key => $value) {
    count($range_arr);
}
unset($value);
echo microtime(true) - $start_t;

0.0054090023040771



サンプル2:

<?php
$start_t = microtime(true);
$range_arr = range(0, 10000);
foreach ($range_arr as $key => &$value) {
    count($range_arr);
}
unset($value);
echo microtime(true) - $start_t;

5.6417820453644

サンプルのように、foreach内で元の配列全体を使用するような場合、参照渡しをしていると恐ろしい性能差が出ます。

というか、これはもう実用に耐えないレベル。

知ってないとこれは引っかかりますねー

種明かしをすると、参照渡しをしないforeachでは実行時の一番初めに配列のコピーを内部的に取りますが、 参照渡しの場合はforeach内で参照されるたびにコピーが取られるようです。
そりゃ遅い。
いかがでしたでしょうか?

2013-02-09 追記
この記事を初めて、掲載した当初は、あまり使ってる人を見かけないマイナーな機能でしたが、 昨今では、foreachの参照渡しは、コーディング規約で禁止するべき、と言う風潮があります。

理由としては、上記のようなことを知らずに使ってバグを生むよりは、多少効率を犠牲にしても、事故の可能性を減らそうと言う感じですね。

効率厨の私には、悲しい時代です。