読者です 読者をやめる 読者になる 読者になる

てきとうなメモ

本の感想とか技術メモとか

配列の参照

から、

問題:以下のコードを実行した結果はどうなるでしょう?

このプログラムの実行結果は?

<?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の値

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のコードを読んだ。構文解析、意味解析の部分のコードは以下の流れで読めるっぽい。

  1. zend_language_parse.yで該当する構文見つける
  2. zend_compile.cでどのopcodeにコンパイルしているかを見る
  3. zend_vm_execute.hでそのopcodeがどの関数を呼んでいるか見る
  4. 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をインクリメントすることしかしていない。