Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

xdebug_debug_zval関数の結果の理由を教えてください。

refcount=0となる理由がわからない

PHP8にて、xdebug_debug_zval関数を使って、リファレンスカウントの原理を確認しています。

以下のコード

$i = 1;
xdebug_debug_zval('i');

を実行した結果が

i: (refcount=0, is_ref=0)=1

となる、特に refcount=0 となる理由を教えてください。

自分の考え

「refcount=1」になると思いました。

理由として、新しい変数コンテナが int 型と値 1 で作成され、そのコンテナを指すシンボル$iが1つあるため、refcountが1になると思いました。

ドキュメントにも以下のようにあります:

「refcount」と呼ばれ、この1つの zval コンテナをどれだけ多くの 変数名(シンボルとも呼ばれます)が指すかを含みます。

補足

しかし int や string などの型の変数は、代入演算子 = によって「元の変数を新しい変数にコピーする (値による代入)」ために、通常の代入では refcount は増えない。

という説明が、(PHPドキュメントではない)他のサイトにありました。

たしかに、”cloneでrefcountが増えない”ということと同様なのかと思えてきました。

しかし、ドキュメントには以下のように記載があります。

$a = "new string";
xdebug_debug_zval('a');

上の例の出力は以下となります。

a: (refcount=1, is_ref=0)='new string'

この変数を他の変数名に代入すると、refcount が増加します。

$a = "new string";
$b = $a;
xdebug_debug_zval( 'a' );

上の例の出力は以下となります。

a: (refcount=2, is_ref=0)='new string'

記載内容が最新の情報でなく、バージョンによる違いなのでしょうか。

0

1Answer

気になったので少し調べてみました。PHPのメモリ管理に詳しいわけではないので誤りがあれば申し訳ございません。

先に結論から申し上げますと、
PHP7から整数型、浮動小数点数型、真偽値型はzval自体に参照カウントを持たなくなった影響により、別の変数から参照されない限りはref_countが0のまま増加しないためです。
語弊の恐れがありますがより直感的に言うと、整数型含むプリミティブ型の変数ではリファレンスカウントを行っていません。

PHP5までは、プリミティブ型と参照型を一括して扱い、コピーオンライト(COW)に基づくメモリ最適化を行っていました。
これは、参照が増えると参照カウントだけを増減させ、実際に変更が行われた場合のみメモリを確保する仕組みです。
しかし、この仕組みは整数や浮動小数点数といったサイズの小さい値に対しては 些か過保護(実際の効率よりもオーバーヘッドが大きくなることがある)でした。

そのため、PHP7では、プリミティブ型と参照型で異なるメモリ管理最適化が採用され、プリミティブ型の変数については基本的に実値をコピーするようになり、参照カウント操作を行わないように変更されています。別の変数に代入しても新しいzvalが生成され、元のzvalの参照カウントが増えることはありません。

記載内容が最新の情報でなく、バージョンによる違いなのでしょうか。

ご推測の通りです。
ドキュメントにはzval コンテナには、変数の型と値の他に、情報の追加ビットを2つ含みますとありますが、これは先述した通りPHP5以前の情報であるため記載内容が古いと言えます。

zvalの構造はPHP5以前とPHP7以降でこのように変化しています。

PHP5のzval

typedef struct _zval_struct {
  union {
    long lval; // 整数と真偽値はここへ
    double dval; // 浮動小数点数はここへ直接格納される
    struct {
      char *val;
      int len;
    } str;
    HashTable *ht;
    struct {
      zend_object_handle handle;
      zend_object_handlers *handlers;
    } obj;
  } value;
  zend_uint refcount; // 型に関係なく一律でここへセットされていた
  zend_uchar type;
  zend_uchar is_ref; // 型に関係なく一律でここへセットされていた
} zval;

PHP7のzval

zvalには直接ref_countを持ちません

typedef struct _zval_struct {
  union {
    long lval; // 整数と真偽値はここへ
    double dval; // 浮動小数点数はここへ直接格納される
    zend_refcounted *counted; // 参照カウントが必要な型のみこちらのヘッダが使用される
    zend_string *str;
    zend_array *arr;
    zend_object *obj;
    zend_resource *res;
    zend_reference *ref; // プリミティブ型であっても参照代入されるとref_countによる管理がおこなれる
    void *ptr;
  } value;
  union {
    struct {
      zend_uchar type;
      zend_uchar flags;
    };
    zend_uint type_info;
  };
  zend_uint reserved;
} zval;

解説サイト:
https://codezine.jp/article/detail/8492?p=2
https://hnw.hatenablog.com/entry/20141207
https://postd.cc/internal-value-representation-in-php-7-part-1/
https://www.phpinternalsbook.com/php7/zvals/basic_structure.html (←こちらの日本語訳サイトがありますが情報が古いので注意してください。)

1Like

Your answer might help someone💌