PHP・GCの話-2話)変数の管理情報、zval containerとreference count


前書き

  • すべての記事は、自分の勉強目的と主観の整理を含めています。あくまで参考レベルで活用してください。もし誤った情報などがあればご意見をいただけるととっても嬉しいです。
  • 内容では、省略するか曖昧な説明で、わかりづらいところもあると思います。そこは、連絡いただければ補足などを追加するので、ぜひ負担なくご連絡ください。
  • 本文での「GC」は、「Garbage Collection, Garbage Collector」の意味しており、略語として使われています。
  • この記事は、連載を前提に構成されています。

※ 連載目録

※ 連載で使うサンプルコード

Sample Code Link on Github

● ExampleGc.php : 2話から6話までの内容で使うサンプルコードです。
● ExampleWeakReference : 7話のWeakReferenceの内容で使うサンプルコードです。

今回の話

今回は、PHPの変数に関して、以下のものを話そうと思うます。

  • 1. 変数宣言とzval container
  • 2. zvalのrefcount, is_ref属性、debug方法
  • 3. zval debug実戦(サンプルコード)
  • 4. Summary

1. 変数宣言とzval container

PHPではzval container 構造体という特別な属性の集合体を定義しています。
一言でいうと、変数の実データと参照情報を持つ属性の集合体と言えます。

もう少し詳しく説明すると、

$str = 'str';

phpで変数を宣言し使うと、PHPは実行時にいろんな作業を行うことになります。
そのうち一つとして、PHPは変数の初期化と共に、変数ごとにzvalという特別な属性の集合体を作り、該当変数と結びつけます。

以下は、変数とzval containerの関係を抽象化し表した図になります。

※ 絵の凡例説明
Code Section : 実際のPHPコードの状態を表します。
Variable Scope : 変数の有効範囲内の変数シンボルの状態を表します。
Heap Space : メモリ内の実際の値の積み重ね(Heap)空間を意味します。

PHPにおいて、すべての変数データは、zval Containerと呼ばれるコンテナーとマッピングされます(以降zvalと呼びます。)。

絵のコードのように、変数をセットすると、※1 変数シンボル(以下、便宜上変数と呼びます)が、現在の変数の有効※2 スコープに生成されます。
同時に、zvalというものが作られ、変数はzvalを示すことになり、zval内のvalue属性として実際の値を持つ構造になっています。※3

このzvalの役目は、変数を管理するために必要な情報を保持させることにあり、valueとtype以外にrefcountとis_refという属性を各変数ごとに保持します。

以下は、phpのソースコードからの引用したコードです(Cの構造体)。
ただし、zvalの構造に関しては、PHPバージョンとデータタイプによって異なる可能性もありますので、心に留めた上でご参考ください。

struct _zval_struct { 
    zvalue_value value;
    zend_uchar type;
    zend_uchar is_ref;
    zend_ushort refcount; 
};

2. zvalのrefcount, is_ref属性、debug方法

zvalの属性の中で、変数データの参照状態を表す属性は、以下の二つがあります。

  • refcount (★重要)
    • integerの値を持ちます。変数が示している「※4 実際の値が参照されているカウント」を意味します。変数自分自身だけだと1,外でも参照されていると2以上になります。
    • GCのメカニズムの核心になる属性
  • is_ref
    • booleanの値を持ちます。変数が※5 reference setである場合、1(true)になります。
    • ※ 当記事では対象外、詳しく扱わない

加えて、zvalの属性はphpでdebugが可能です。
phpでは、以下の関数を提供しています。

debug_zval_dump ( mixed $variable [, mixed $... ] ) : void

xdebug拡張では、以下の関数を提供しています。

xdebug_debug_zval( string ...$varname ) : void

本記事では、xdebug_debug_zvalを利用します。

3. zval debug実戦(サンプルコード付き)

では、いろんなタイプの変数のzvalを直接debugしてみましょう。
Value Copy & Reference Copyに加えて、refcountの変化をよくみていただけると良いと思います。

サンプルコードをご参考になる方は、以下のURLで開かれるラインから確認いただけます。
- GITHUB SOURCE CODE LINK -

PHPでサポートしているデータタイプの詳細な説明は省略します。

もしまだ詳しくない方は、以下のPHP Language Reference→Typesを参考すると良いと思います。
https://www.php.net/manual/en/language.types.php

1) stringのzval属性 (Scalar TypeのValue Copy)

$str = "sometext";
$toBeCopiedAsString = $str;
xdebug_debug_zval('str');
xdebug_debug_zval('toBeCopiedAsString');

/* --OUTPUT
str: (refcount=1, is_ref=0)='sometext'
toBeCopiedAsString: (refcount=1, is_ref=0)='sometext'
*/

基本的な構造になります。
stringのようなScalar Valueの変数を他の変数にアサインする時には、基本Vaule Copyで動作します。※6
なので、PIC-2-1のように実際の値のsometextは、メモリ空間に2個存在することになります。
そして、$str$toBeCopiedAsStringの変数は、各自違うメモリ内の値を示すことになるので、refcount=1になります。

2) objectのzval属性 (Compound TypeのReference Copy)

Log::debug(null, ['event' => 'set', 'msg' => 'reference copy of object']);
$object = new \stdClass;
$toBeRefCopyFromObject = $object;
$toBeUnset = $object;
xdebug_debug_zval('object');
xdebug_debug_zval('toBeRefCopyFromObject');
xdebug_debug_zval('toBeUnset');

Log::debug(null, ['event' => 'unset', 'msg' => 'reference copy of object']);
unset($toBeUnset);
xdebug_debug_zval('object');
xdebug_debug_zval('toBeRefCopyFromObject');
xdebug_debug_zval('toBeUnset');

/* --OUTPUT
[2020-08-12 13:21:02] local.DEBUG:  {"event":"set","msg":"reference copy of object"}
object: (refcount=3, is_ref=0)=class stdClass {  }
toBeRefCopyFromObject: (refcount=3, is_ref=0)=class stdClass {  }
toBeUnset: (refcount=3, is_ref=0)=class stdClass {  }
[2020-08-12 13:21:02] local.DEBUG:  {"event":"unset","msg":"reference copy of object"}
object: (refcount=2, is_ref=0)=class stdClass {  }
toBeRefCopyFromObject: (refcount=2, is_ref=0)=class stdClass {  }
toBeUnset: no such symbol
*/

今回は、classをnewしたobjectを示す変数になります。
object変数は、他の変数にアサインするときには、基本Reference Copyで動作します。
なのでValue Copyとは違い、絵2のように実際の値であるobjectは、メモリ空間には1個存在することになります。
そして、$object$toBeRefCopyFromObject$toBeUnsetの変数は、同じメモリ内の値を示すことになるので、もとの値のzvalのrefcountが1ずつ増加し、refcount=3になります。

その後、$toBeUnset※7 unsetし、シンボルのレファレンスを解除します。
そうすると、もとの値を参照する変数が一つなくなったので、refcountは1減少します。

なので、最終的にrefcount=2になります。

3) arrayのzval属性 (Compound TypeのReference Copy & Value Copy)

Log::debug(null, ['event' => 'set', 'msg' => 'reference copy of array']);
$array = array_fill(0,2,0);
$toBeRefFromArray = $array;
xdebug_debug_zval('array');
xdebug_debug_zval('toBeRefFromArray');

Log::debug(null, ['event' => 'set', 'msg' => 'changing value copy of array']);
$array[1] = 1;
xdebug_debug_zval('array');
xdebug_debug_zval('toBeRefFromArray');

/* --OUTPUT
[2020-08-12 13:00:23] local.DEBUG:  {"event":"set","msg":"reference copy of array"}
array: (refcount=2, is_ref=0)=array (0 => (refcount=0, is_ref=0)=0, 1 => (refcount=0, is_ref=0)=0)
toBeRefFromArray: (refcount=2, is_ref=0)=array (0 => (refcount=0, is_ref=0)=0, 1 => (refcount=0, is_ref=0)=0)

[2020-08-12 13:00:23] local.DEBUG:  {"event":"set","msg":"changing value copy of array"}
array: (refcount=1, is_ref=0)=array (0 => (refcount=0, is_ref=0)=0, 1 => (refcount=0, is_ref=0)=1)
toBeRefFromArray: (refcount=1, is_ref=0)=array (0 => (refcount=0, is_ref=0)=0, 1 => (refcount=0, is_ref=0)=0)
*/

最後に、arrayを示す変数になります。
arrayは、他の変数にアサインするときには、基本Reference Copyで動作します。
しかしarrayは、配列内の値を変えると、PHP内部的にValue Copyを行い、新しいarrayをメモリ空間に生成する特性があります。

なので、最初は、refcount=2で、同じメモリ空間の値を示していたものが、値を変えた瞬間、お互いに違うarrayを示すことになりrefcount=1になっています。
arrayの内容も$arrayは値を変更下(0,1)で、$toBeRefFromArrayはもともとの値である(0,0)になっています。

4. Summary

今回で、最低限に覚えて頂くと良い内容は、以下になります。

  • 変数を定義すると、実際のデータと共に、「変数シンボル」と「zval」が作られる。
  • zvalは、変数を管理するための情報を持っている。
  • (重要) zval属性のrefcountは、Reference Copyが起きると1増加し、変数が無効になると1減少する。※8

後書き

ここまで、変数宣言とzval属性、変数の参照とその変化に関して見ました。
メモリ観点において、変数を宣言した時に何が起きるのかという観点は時々大事な観点になると思います。

そして、開発言語ごとに、変数の初期化と管理メカニズムは共通するところもあれば、違うところもあるので、他の言語と比較してみるのも面白いでしょう。

今回は、次回の話の「変数データの消滅条件」を扱うことになりますが、前提としては参照カウント(phpではrefcount)の理解が必要になっているので、zvalとReference Copyに関して説明する話を用意いたしました。

では、次回も頑張って行きます。

※注釈

※1
▶ 変数シンボル

$obj=new stdClass;にした場合、$objが変数シンボル、new stdClassで生成されたobject(instance)が、実際の値になります。$objの部分は、「変数のシンボル」が正確な名称かもですが、以降、便宜上変数と呼ぶようにします。

※2
▶ スコープ
※ 本連載記事でのスコープの意味は、主に「ローカルスコープ、スコープスタック、全域スコープ」を全て含めています。

スコープとは、いろんな言語で「変数の有効範囲」して定義します。PHPでは「シンボルテーブル」という概念が正しいですが、本記事では理解の便宜上スコープと呼ぶようにします。もし、詳しく見たい方は、PHPのデータ構造メカニズムを探してみると、シンボルテーブルに関する説明も出てくるのでおすすめです。

※3
▶ zval内のvalue属性として実際の値を持つ構造になっています。

ここでは、zvalのみ表記していますが、色々省略されています。実は、symbol tableを始め、zval struct, zval value structなど色々あり、データタイプによって、また色々連携された構造を持つことになります。詳しく見たい方は、PHPのデータ構造メカニズムを探してみるとzval struct, zval value structなどを詳細に解説した資料があるのでおすすめします。php5と7では、またメカニズムが違ってくるので、難しいですね(泣)

※4
▶ 実際の値

ここでの実際の値と言うのは、Literal値はもちろん、array, objectなどで実際にメモリに保存されている値, resourceの実際資源などの示しています。

※5
▶ reference set

reference setとは、「&」演算子を使いレファレンスとして定義された変数を意味します(assigning by reference)。reference setとして定義する場合、新しく定義した変数と被参照変数両方is_ref=1になります。
phpには、レファレンスタイプを扱う中で、Reference Copyと、Reference Setのタイプがありますが、その定義と違いに関しては、今回のGCのテーマとは少し範囲が違うので省略させていただきます。もしリクエストがある場合は、別記事として上げていただきますので、ご連絡ください。

※6
▶ Scalar Valueの変数を他の変数にアサインする時には、基本Vaule Copyで動作します。

補足として、Scalar Typeの一つであるintegerは、また特殊な動作を行いますので、気になる方はデバッグしてみてください。(最小は定数として動作し、&演算子とかでReference Setにすると、参照変数に変わります。自分もすべてのパターンは把握していないので、説明するのは少し難しいですね。笑)

※7
▶ unset

unset()は、レファレンスを持つ変数シンボルを変数有効範囲から解除する関数です。実際のメモリの値を消す関数では無いことに注意しましょう。unsetする時に、参照するところが0(つまりrefcount=0)の場合は、内部的にすぐメモリから実際の値も解除されますが、refcountが1以上だと、その値はメモリから削除されずずっと残っている状態になります。このメカニズムに関しては、後ほど扱う予定なので、まずはと止めて置くだけで大丈夫です。

※8
▶ zval属性のrefcountは、Reference Copyが起きると1増加し、変数が無効になると1減少する。

補足ですが、Reference Setが起きる場合もzvalのrefcount+1で、is_refが1に変化します。Reference Setに対するzval属性の変化も理解しておけば良いのですが、GCの理解においては必須ではありません。