から、
問題:以下のコードを実行した結果はどうなるでしょう?
このプログラムの実行結果は?
<?php $array[0] = 1; $array[1] = 2; $array[2] = 3; $ref = &$array[1]; //参照渡し $copy = $array; //値渡し $copy[0] = 'a'; $copy[1] = 'b'; $copy[2] = 'c'; print_r($array); //←この出力は? ?>正解は以下のようになる。
Array ( [0] => 1 [1] => b [2] => 3 )
<?php $arr1 = array(0,1); $tmp = &$arr1[0]; $arr2 = $arr1; $arr2[0] = 'a'; $arr2[1] = 'b'; var_dump($arr1); ?>これと、
<?php $a = 0; $tmp = &$a; $b = $a; $b = 'a'; var_dump($a); ?>の挙動の違いに関して説明できるのがベストです。
質問時に挙げたサイトの説明だと下の例が0を出力する説明ができません。
PHPの値
PHPの値はzvalという構造体からなっていて、s_ref__gcとrefcount__gcというメンバがくっついている。
struct _zval_struct { /* Variable information */ zvalue_value value; /* value */ zend_uint refcount__gc; zend_uchar type; /* active type */ zend_uchar is_ref__gc; };
valueは数値とか文字列と配列とか実際の値が入っている共用体である。
typedef union _zvalue_value { long lval; /* long value */ double dval; /* double value */ struct { char *val; int len; } str; HashTable *ht; /* hash table value */ zend_object_value obj; } zvalue_value;
is_refは参照かどうかを表していて、
<?php $b =& $a; ?>
を実行すると、$bと$aは、同じzvalを指し、zvalのis_refは1になる。
refcountは参照カウンタを意味している。PHPの値はcopy on writeになっていて、
<?php $a = 1; $b = $a; $a = 2; ?>
の時、$b = $aを実行した段階では$bと$aは同じzvalを指しており、zvalのrefcountは2になる。
そして、$a = 2を実行したときに、$aはmallocした新領域を指すようにして、そこに値2をコピーする。これは右辺$aの指していた値のrefcountを見て1でないので、コピーすべきと判断しているのである。
スカラー値の場合
で、本題に入って、まず以下のスクリプトが何を実行しているかを見る
<?php $a = 0; $tmp = &$a; $b = $a; $b = 'a'; var_dump($a); ?>
$ php scalar.php int(0)
xdebug_debug_zvalを利用してis_refとrefcountの値を見てみる
<?php $a = 0; echo ("\$a = 0;\n"); xdebug_debug_zval( 'a' ); $tmp = &$a; echo ("\$tmp = &\$a;\n"); xdebug_debug_zval( 'a' ); xdebug_debug_zval( 'tmp' ); $b = $a; echo ("\$b = \$a;\n"); xdebug_debug_zval( 'a' ); xdebug_debug_zval( 'b' ); xdebug_debug_zval( 'tmp' ); $b = 'a'; echo ("\$b = 'a';\n"); xdebug_debug_zval( 'a' ); xdebug_debug_zval( 'b' ); xdebug_debug_zval( 'tmp' ); var_dump($a); ?>
$ php scalar.php $a = 0; a: (refcount=1, is_ref=0)=0 $tmp = &$a; a: (refcount=2, is_ref=1)=0 tmp: (refcount=2, is_ref=1)=0 $b = $a; a: (refcount=2, is_ref=1)=0 b: (refcount=1, is_ref=0)=0 tmp: (refcount=2, is_ref=1)=0 $b = 'a'; a: (refcount=2, is_ref=1)=0 b: (refcount=1, is_ref=0)='a' tmp: (refcount=2, is_ref=1)=0 int(0)
$tmp =& $aを実行した時に、$tmpと$aは同じzvalを指し、refcountは2、is_refは1になる。
$b = $aを実行した時には値のコピーが発生する。これはis_ref=1である値を代入した場合はrefcountに無条件でコピーするようなコードになっているためである。このコピーにおいてはis_refやrefcountはコピーされずに初期化される。=は値のコピーを意味すると考えるとこれは自然かなと思う。
で、$bと$aが別のzvalを指しているので、$b='a'を実行しても$aは0のままである。
配列の参照の場合
次に配列の要素の参照の場合であるが、ちょっと説明のため改変してみた。
<?php $arr1 = array(0,1,2); $tmp = &$arr1[0]; $arr2 = $arr1; $arr2[1] = 'b'; $arr2[0] = 'a'; var_dump($arr1); ?>
$ php array.php array(3) { [0]=> &string(1) "a" [1]=> int(1) [2]=> int(2) }
xdebug_debug_zvalでis_refとrefcountを表示してみる
<?php $arr1 = array(0,1,2); echo("\$arr1 = array(0,1,2);\n"); xdebug_debug_zval('arr1'); $tmp = &$arr1[0]; echo("\$tmp = &\$arr1[0];\n"); xdebug_debug_zval('arr1'); xdebug_debug_zval('tmp'); $arr2 = $arr1; echo("\$arr2 = \$arr1;\n"); xdebug_debug_zval('arr1'); xdebug_debug_zval('arr2'); xdebug_debug_zval('tmp'); $arr2[1] = 'b'; echo("\$arr2[1] = 'b';\n"); xdebug_debug_zval('arr1'); xdebug_debug_zval('arr2'); xdebug_debug_zval('tmp'); $arr2[0] = 'a'; echo("\$arr2[0] = 'a';\n"); xdebug_debug_zval('arr1'); xdebug_debug_zval('arr2'); xdebug_debug_zval('tmp'); var_dump($arr1); ?>
$ php array.php $arr1 = array(0,1,2); arr1: (refcount=1, is_ref=0)=array (0 => (refcount=1, is_ref=0)=0, 1 => (refcount=1, is_ref=0)=1, 2 => (refcount=1, is_ref=0)=2) $tmp = &$arr1[0]; arr1: (refcount=1, is_ref=0)=array (0 => (refcount=2, is_ref=1)=0, 1 => (refcount=1, is_ref=0)=1, 2 => (refcount=1, is_ref=0)=2) tmp: (refcount=2, is_ref=1)=0 $arr2 = $arr1; arr1: (refcount=2, is_ref=0)=array (0 => (refcount=2, is_ref=1)=0, 1 => (refcount=1, is_ref=0)=1, 2 => (refcount=1, is_ref=0)=2) arr2: (refcount=2, is_ref=0)=array (0 => (refcount=2, is_ref=1)=0, 1 => (refcount=1, is_ref=0)=1, 2 => (refcount=1, is_ref=0)=2) tmp: (refcount=2, is_ref=1)=0 $arr2[1] = 'b'; arr1: (refcount=1, is_ref=0)=array (0 => (refcount=3, is_ref=1)=0, 1 => (refcount=1, is_ref=0)=1, 2 => (refcount=2, is_ref=0)=2) arr2: (refcount=1, is_ref=0)=array (0 => (refcount=3, is_ref=1)=0, 1 => (refcount=1, is_ref=0)='b', 2 => (refcount=2, is_ref=0)=2) tmp: (refcount=3, is_ref=1)=0 $arr2[0] = 'a'; arr1: (refcount=1, is_ref=0)=array (0 => (refcount=3, is_ref=1)='a', 1 => (refcount=1, is_ref=0)=1, 2 => (refcount=2, is_ref=0)=2) arr2: (refcount=1, is_ref=0)=array (0 => (refcount=3, is_ref=1)='a', 1 => (refcount=1, is_ref=0)='b', 2 => (refcount=2, is_ref=0)=2) tmp: (refcount=3, is_ref=1)='a' array(3) { [0]=> &string(1) "a" [1]=> int(1) [2]=> int(2) }
$tmp =& $arr1[0]を実行することで、$tmpと$arr1[0]は同じzvalを指し、is_refは1、refcountは2になる。
$arr2 = $arr1を実行すると、copy on writeの実装のため、コピーは発生せず、$arr2と$arr1は同じzvalを指し、refcountは2となる。配列そのものも配列の要素も$arr2と$arr1で同じzvalを指している。
$arr2[1] = 'b';を実行すると、配列が修正されるので、copy on writeでコピーが実行される。ただし、配列そのもののzvalが別ものになりコピーされるのだが、配列の各要素のzvalは$arr2と$arr1で同じものをさしたまま、refcountがインクリメントされる。このタイミングでzvalを表示すると以下のようになるだろう。
arr1: (refcount=1, is_ref=0)=array (0 => (refcount=3, is_ref=1)=0, 1 => (refcount=2, is_ref=0)=1, 2 => (refcount=2, is_ref=0)=2) arr2: (refcount=1, is_ref=0)=array (0 => (refcount=3, is_ref=1)=0, 1 => (refcount=2, is_ref=0)=1, 2 => (refcount=2, is_ref=0)=2) tmp: (refcount=3, is_ref=1)=0
そして、$arr2[1] = 'b'が実行されるのだが、$arr2[1]のzvalのrefcountが2であるためコピーが発生し、$arr1[1]と$arr2[1]は別のzvalを指すようになる。zvalのrefcountは共に1になる。
さらに、$arr2[0] = 'a'を実行すると、$arr2[0]のzvalのis_refを見てこれは参照であることがわかるので、'a'をzval.valueに上書きする。
というわけで、$arr1[0]と$arr2[0]と$tmpは、ずっと同じzvalを指しているので更新されてしまうのである。
何が違うのか、何が問題なのか
$arr2 = $arr1とやっている時に配列をコピーしているのだけども、要素に対しては各要素のrefcountインクリメントしているだけなのが問題かなと思った。参照となっているzvalを'='で代入するときはコピーになるという部分で一貫性を保つならば、refcountをインクリメントするのではなく、参照かどうかを確認してコピーするとかした方が良いんじゃないかなと
ソースコード的な話
php5.4.12のコードを読んだ。構文解析、意味解析の部分のコードは以下の流れで読めるっぽい。
- zend_language_parse.yで該当する構文見つける
- zend_compile.cでどのopcodeにコンパイルしているかを見る
- zend_vm_execute.hでそのopcodeがどの関数を呼んでいるか見る
- zend_execute.cで実際の処理を確認する
参照代入はの処理はzend_assign_to_variable_reference、代入の処理はzend_assign_to_variableの所に書かれてある。
配列の要素に代入する時は最初にzend_fetch_dimension_addressが呼ばれて、配列そのもののコピーが発生する
配列などをコピーする場合はコピーコンストラクタが呼ばれる。このコピーコンストラクタはzend_variables.cの_zval_copy_ctor_func
配列の場合は、zend_valueはHashTable* htを利用しており、HashTableに対する操作はzend_hash.cに記述されている。zvalに依存しないようなコードになっているが、ハッシュ表のコピー関数zend_hash_copyにはコピーコンストラクタを引数に渡せる。ただし、コードではrefcountをインクリメントすることしかしていない。