PHP変数の参照とカウントのルール

WBOY
リリース: 2016-06-20 12:46:35
オリジナル
904 人が閲覧しました

変数の内部参照とカウント

エンジン内では、PHP 変数は「zval」構造体に格納されます。この構造体には、変数の型と値の情報が含まれています。記事「変数の内部ストレージ: 値と型」ですでに紹介したように、この構造体には他に 2 つのフィールド情報があり、1 つは「is_ref」(バージョン 5.3.2 ではこのフィールドは is_ref__gc) で、このフィールドはブール値であり、使用されます。変数が参照であるかどうかを識別するために、PHP エンジンはこのフィールドを通じて一般変数と参照変数を区別できます。 PHP コードでは、& 演算子記号を使用して参照変数を作成できます。作成された参照変数内の zval の is_ref フィールドは 1 です。 zval には別のフィールド refcount があります (このフィールドはバージョン 5.3.2 では refcount__gc です)。このフィールドはカウンタであり、この zval コンテナを指す変数名の数を示します。このフィールドが 0 の場合は、指す変数がないことを意味します。この zval を解放すると、エンジン内のメモリが最適化されます。次のコードを考えてみましょう。

<!--?php  $a = "Hello NowaMagic";  $b = $a;  ?-->
ログイン後にコピー

コードには 2 つの変数 $a と $b があり、$a は通常の代入によって $b に割り当てられるため、$b の値は $a と等しくなります。 $ の場合、 b の変更は $a には影響を与えないため、このコードで $a と $b が 2 つの異なる zval に対応する場合、それは明らかにメモリの無駄であり、PHP 開発者はそれを許可しません。これが起こります。したがって、実際には $a と $b は同じ zval を指します。この zval の型は STRING で、その値は "Hello world" です。これを指す 2 つの変数 $a と $b があるため、通常の代入であるため、is_ref フィールドは 0 になります。 これによりメモリのオーバーヘッドが節約されます。

$a = "Hello world" を実行した後、$a に対応する zval 情報は次のとおりです: a: (refcount=1, is_ref=0)="Hello world"

ただし、その後に実行します$b=$a、$a に対応する zval 情報は次のとおりです: a: (refcount=2, is_ref=0)="Hello world"

以下の以前のコードを変更します:

<?php  $a = "Hello world";  $b = &$a;  ?>
ログイン後にコピー

このように、参照代入により$aが$bに代入されます。

$a = "Hello world" を実行した後、$a に対応する zval 情報は次のとおりです: a: (refcount=1, is_ref=0)="Hello world"

ただし、その後に実行します$b=&$a、$a に対応する zval 情報は次のとおりです: a: (refcount=2, is_ref=1)="Hello world"

is_ref フィールドが 1 に設定されていることがわかります。 $a と $b に対応する zval は参照です。このようにして、エンジンにおける変数の参照とカウントについての基本的な理解が得られました。変数の分離については以下で説明します。

書き込み時の変数の分離

上記のコードの最初の部分を検討し、通常の方法で $a を $b に割り当てます。この時点でも、2 つの内部変数は同じ zval を指します。 $b の値を「new string」に変更しても、$a 変数の値は「Hello world」のままです。

<?php  $a = "Hello world";  $b = $a;  $b = "new string";  echo $a;  echo $b;  ?>
ログイン後にコピー

$a と $b は明らかに同じ zval を指します。なぜそれを変更したのでしょうか? $b と $a は変更されないままであることができます。簡単に言うと、$b が再割り当てされると、$b は以前の zval から分離されます。分離後、$a と $b はそれぞれ異なる zval を指します。

コピーオンライト テクノロジのよく知られたアプリケーションは、Unix オペレーティング システム カーネルにあります。プロセスが fork 関数を呼び出して子プロセスを生成すると、親プロセスと子プロセスは同じアドレス空間の内容を持ちます。古いバージョンでは、子プロセスがフォークするときに親プロセスのアドレス空間の内容をコピーします。さらに悪いことに、このプロセスには多くのオーバーヘッドが発生する可能性があります。フォークした後、子プロセスで exec を直接呼び出して別のプログラムを実行します。これにより、元々親プロセスからコピーするのに時間がかかったアドレス空間が、以前の新しいプロセスのアドレス空間に置き換えられます。これは明らかにリソースの極端な浪費であるため、後のシステムでは、フォーク後も、子プロセスのアドレス空間は単にそのアドレスを指すようになりました。子プロセスがアドレス空間の内容を書き込む必要がある場合にのみ、子プロセス用に別のコピー (通常はメモリ ページ単位) が分離されます。子プロセスがすぐに exec 関数を呼び出した場合でも、親プロセスのアドレス空間から内容をコピーする必要がないため、メモリが節約され、同時に速度が向上します。

$b が $a が指す zval から分離されると、zval の refcount が 1 減らされ、前の 2 が 1 になります。これは、この zval にもそれを指す変数があることを意味します。 、これは $a です。 $b 変数は新しい zval を指し、新しい zval の refcount は 1 で、値は文字列 "new string" です。おおよそのプロセスは次のとおりです。

$a = "Hello world" 	//a: (refcount=1, is_ref=0)="Hello world"$b = $a       		//a,b: (refcount=2, is_ref=0)="Hello world"$b = "new string" 	//a: (refcount=1, is_ref=0)="Hello world"   b: (refcount=1, is_ref=0)="new string"(发生分离操作)
ログイン後にコピー

この分離ロジックは次のようになります。 a の場合、一般変数 a (isref=0) は一般的な代入操作を実行します。a が指す zval の count refcount が 1 より大きい場合、新しい zval を a に再代入する必要があります。前の zval の count refcount が 1 減ります。

上記は通常の代入の場合です。参照代入の場合は、次の変更プロセスを見てみましょう。

$a = "Hello world" 	//a: (refcount=1, is_ref=0)="Hello world"$b = &$a       	//a,b: (refcount=2, is_ref=1)="Hello world"$b = "new string" 	//a,b: (refcount=2, is_ref=1)="new string"
ログイン後にコピー

可以看出来,对一个引用类型的zval进行赋值是不会进行分离操作的,实际上我们再产生一个引用变量的时候是可能出现一个分离操作的,只是时机有些不同:

  1. 在普通赋值的情况下,分离操作发生在$b="new string"这一步,也就是在对变量赋新的值的时候,才会进行zval分离操作

  2. 在引用赋值的情况下,分离操作有可能发生在$b = &$a这一步,也就是在生成引用变量的时候

情况1就不多解释了,情况2中强调是有可能发生分离,以前面的这代码为例子,是否进行分离与$a当前指向的zval的refcount有关系,代码中$b = &$a 的时候, $a指向的zval的refcount=1,这个时候不需要进行分离操作,但是如果refcount=2,那么就需要分离一个zval出来。比如如下代码:

<?php  $a = "Hello world";  $c = $a;  $b = &$a;  $b = "new string";  ?>
ログイン後にコピー

在执行引用赋值的时候,$a指向的zval的refcount=2,因为$a和$c同时指向了这个zval,所以在$b=&$a的时候,就需要进行一个分离操作,这个分离操作生成了一个ref=1的zval,并且计数为2,因为$a,$b两个变量指向分离出来的zval,原来的zval的refcount减少1,所以最终只有$c指向一个值为"Hello world",ref=0的zval1, $a和$b指向一个值为"Hello world",ref=1的zval2。 这样我们对$c的修改时在操作zval1,对$a和$b的修改都是在操作zval2,这样就符合引用的特性了。

此过程大致如下:

$a = "Hello world";	//a: (refcount=1, is_ref=0)="Hello world"$c  = $a;       	// a,c: (refcount=2, is_ref=0)="Hello world"$b = &$a;       	// c: (refcount=1, is_ref=0)="Hello world" a,b: (refcount=2, is_ref=1)="Hello world" (发生分离操作)$b = "new string"; 	// c: (refcount=1, is_ref=0)="Hello world" a,b: (refcount=2, is_ref=1)="new string"
ログイン後にコピー

试想一下如果不进行这个分离会有什么后果?如果不进行分离,$a,$b,$c都指向了同一个zval,对$b的修改也会影响到$c,这显然是不符合PHP语言特性的。

这个分离逻辑可以表述为:将一个一般变量a(isref=0)的引用赋给另外一个变量b的时候,如果a的refcount大于1,那么需要对a进行一次分离操作,分离之后的zval的isref等于1,refcount等于2

通过以上的一些知识和分离逻辑读者应该可以很容易分析其它的一些情况。比如将一个引用变量a(isref=1)的引用赋给一般变量b的时候,需要将b之前指向的zval的refcount减少1,然后将b指向a的zval,a的zval的refcount加1,没有任何分离操作

这些理论结合实际代码会让你更容易理解这个过程。

unset的作用

unset()并非一个函数,而是一种语言结构,这个可以通过查看编译生成的opcode看到区别,unset对应的不是一个函数调用的opcode。那么unset到底做了什么? 在unset对应的opcode的handler中可以看到相关内容,主要的操作时从当前符号表中删除参数中的符号,比如在全局代码中执行unset($a),那么将会在全局符号表中删除a这个符号。全局符号表是一张哈希表,建立这张表的时候会提供一个表中的项的析构函数,当我们从符号表中删除a的时候,会对符号a指向的项(这里是zval的指针)调用这个析构函数,这个析构函数的主要功能是将a对应的zval的refcount减1,如果refcount变成了0,那么释放这个zval。所以当我们调用unset的时候,不一定能释放变量所占的内存空间,只有当这个变量对应的zval没有别的变量指向它的时候,才会释放掉zval,否则只是对refcount进行减1操作。


ソース:php.cn
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート