内部からPHPを学習します。 ズヴァル

この記事は、私が現在ロシア語に翻訳しているPHP Internals Bookの Zvals章に基づいています[ 1 ]。 この本の主な目的は、PHPの拡張機能を作成したいCプログラマーですが、インタープリターの内部ロジックについて説明しているので、PHP開発者にとって役立つと確信しています。 この記事では、基本的な理論のみを残しましたが、これはすべての開発者に明らかなはずです(PHPやCにも精通していません)。 資料のより完全なプレゼンテーションについては、本を参照してください。



タスクは注意を引くことです。 次のコードを実行した結果はどうなりますか?

$obj1 = new StdClass(); $obj2 = new StdClass(); $obj1->value = 1; $obj2->value = 1; function f1($o) { $o = 100; } function f2($o) { $o->value = 100; } f1($obj1); f2($obj2); var_dump($obj1); var_dump($obj2);
      
      







答え
オブジェクト(stdClass)#1(1){["value"] => int(1)}

オブジェクト(stdClass)#2(1){["value"] => int(100)}



答えを正確に定義し、その理由を説明できる場合は、おそらくこの記事から新しいことを学ぶことはないでしょう。さもなければ、この記事を読んで知識を深めてください。



基本構造


PHPの基本的なデータ構造はzval(「Zend value」の略)です。 各zvalはそれ自体にいくつかのフィールドを格納し、そのうちの2つはこの値の値とタイプです。 PHPは動的型付け言語であり、したがって変数の型はコンパイル時ではなく実行時にのみ認識されるため、これが必要です。 さらに、変数の型はzvalの有効期間中に変更できます。つまり、以前に整数として保存されていたzvalは後で文字列を含むことができます。



変数の型は整数ラベル(型タグ、符号なし文字)として保存されます。 ラベルは、PHPで使用可能な8つのデータ型に対応する8つの値のいずれかを取ることができます。 これらの値は、 IS_TYPE



形式の定数を使用して割り当てる必要があります。 たとえば、 IS_NULL



はNULLデータ型にIS_STRING



し、 IS_STRING



は文字列にIS_STRING



します。



zvalue_value


変数の実際の値は、次のように定義されるユニオンデータ型(「ユニオン」、以下ではユニオンまたはユニオンという用語を使用します)に格納されます。

 typedef union _zvalue_value { long lval; double dval; struct { char *val; int len; } str; HashTable *ht; zend_object_value obj; } zvalue_value;
      
      





組合の概念に精通していない人のための小さな説明。 Unionは、さまざまなタイプの複数のデータメンバーを定義しますが、ユニオンで定義された値からは常に1つの値しか使用できません。 たとえば、 value.lval



がデータメンバーvalue.lval



に割り当てられた場合、 value.lval



のみを使用してデータにアクセスできます。他のデータメンバーへのアクセスは受け入れられず、プログラムの予期しない動作につながる可能性があります。 これは、ユニオンがすべてのメンバーのデータを同じメモリ領域に保存し、アクセスしている名前に基づいて値を異なる方法で解釈するためです。 ユニオンに割り当てられたメモリのサイズは、その最大データメンバーのサイズに対応します。



zval-sを使用する場合、特別なタグ(タイプタグ)が使用されます。これにより、現在ユニオンに格納されているデータのタイプを判別できます。 APIに移る前に、PHPでサポートされているデータ型とその保存方法を確認しましょう。



最も単純なデータ型はIS_NULL



ですIS_NULL



であるため、値を格納しないでください。



数値を格納するために、PHPは2つのタイプを表します: IS_LONG



およびIS_DOUBLE



double dval



それぞれlong lval



およびdouble dval



使用します。 最初は整数を保存するために使用され、2番目は浮動小数点数のために使用されます。



longデータ型について知っておくべきことがいくつかあります。 まず、符号付き整数です。つまり、正の値と負の値を含むことができますが、このデータ型はビット演算には適していません。 次に、longはプラットフォームごとにサイズが異なります。32ビットシステムではサイズが32ビットまたは4バイトですが、64ビットシステムではサイズが4または8バイトになります。 Unixシステムでは通常8バイトのサイズですが、64ビットバージョンのWindowsでは4バイトしか使用しません。



このため、long型の特定の値に依存しないでください。 longデータ型に格納できる最小値と最大値は、 LONG_MIN



およびLONG_MAX



で使用でき、この型のサイズはSIZEOF_LONG



マクロを使用して決定できます( sizeof(long)



とは異なり、このマクロは#if



ディレクティブでも使用できます)。



double



データ型は、浮動小数点数を格納するためのものであり、通常IEEE-754仕様に従って、8バイトのサイズを持っています。 この形式の詳細についてはここでは説明しませんが、少なくともこの型の精度は制限されており、多くの場合、期待している値を正確に保存しないことに注意してください。



ブール変数はIS_BOOL



フラグを使用し、値0



(偽)および1



(真)としてlong val



フィールドに格納されます。 2つの値のみがこの型を使用するため、理論的にはより小さい型(たとえば、zend_bool)を使用しても十分ですが、zvalue_valueはユニオンであり、最大データメンバーに対応するメモリサイズが割り当てられ、よりコンパクトな変数を適用しますブール値の場合、メモリは節約されません。 したがって、この場合はlvalが再利用されます。



行( IS_STRING



)は、構造struct {char *val; int len; } str;



struct {char *val; int len; } str;



つまり、文字列は、文字列char *



および文字列int



整数長へのポインタとして格納されます。 PHPの文字列は、NULバイト(\ 0)を含むことができ、バイナリセーフであるために、長さを明示的に保存する必要があります。 ただし、これにもかかわらず、PHPで使用される文字列は、文字列の長さの引数をとらないライブラリ関数との互換性を確保するために、NULで終了するバイトで終了しますが、文字列の最後にnullバイトが見つかると想定しています もちろん、このような場合、文字列はバイナリセーフではなくなり、ゼロバイトが最初に現れるまで切り捨てられます。 たとえば、多くのファイルシステム関連の関数とlibcのほとんどの文字列関数は同様に動作します。



文字列の長さは(Unicode文字の数ではなく)バイト単位で測定され、ゼロバイトを含むべきではありません。つまり、文字列foo



長さは、格納に4バイトが使用されるにもかかわらず3です。 sizeof



を使用して文字列の長さを決定する場合、1を減算する必要があります: strlen("foo") == sizeof("foo") - 1







理解することが非常に重要です。文字列の長さは、long型または他の類似の型ではなく、int型に格納されます。 これは、文字列の長さを2147483647バイト(2ギガバイト)に制限する歴史的なアーティファクトです。 より大きな行は、オーバーフローを引き起こします(長さが負になります)。



残りの3つのタイプは、表面的にのみ言及されます。



配列はIS_ARRAY



ラベルを使用し、 HashTable *ht



データHashTable *ht



格納されます。 HashTableデータ構造の仕組みについては、別の記事で説明しています。



オブジェクト( IS_OBJECT



)はzend_object_value obj



データzend_object_value obj



使用します。これは、「オブジェクトハンドル」(実際のデータの検索に使用される整数ID)と、オブジェクトの動作を決定する「オブジェクトハンドラー」のセットで構成されます。 PHPのクラスとオブジェクトのシステムについては、「クラスとオブジェクト」の章で説明します。



リソース( IS_RESOURCE



)は、値の検索に使用される一意のIDも格納するため、オブジェクトに似ています。 このIDは、長いlvalメンバーに保存されます。 リソースについては、まだ書かれていない対応する章で説明します。



小計を要約すると、使用可能なすべてのタイプラベルとそれに対応する値ストアをリストした表が以下にあります。

タイプタグ 保管場所
IS_NULL



none



IS_BOOL



long lval



IS_LONG



long lval



IS_DOUBLE



double dval



IS_STRING



struct { char *val; int len; } str



IS_ARRAY



HashTable *ht



IS_OBJECT



zend_object_value obj



IS_RESOURCE



long lval





zval


次に、zvalデータ構造がどのように見えるかを見てみましょう。



 typedef struct _zval_struct { zvalue_value value; zend_uint refcount__gc; zend_uchar type; zend_uchar is_ref__gc; } zval;
      
      





既に述べたように、zvalには値とその型を格納するためのメンバーが含まれています。 値は、上記のzvalue_valueユニオンに格納されます。 タイプはzend_uchar type



保存されます。 さらに、この構造には、名前が__gc



で終わる2つの追加のプロパティが含まれています。これらのプロパティは、ガベージコレクションメカニズムによって使用されます。 これらのプロパティについては、次のセクションで詳しく説明します。



メモリ管理


zvalデータ構造は2つの役割を果たします。 まず、前のセクションで説明したように、データとそのタイプを保存します。 第二に(これについては現在のセクションで説明します)、メモリ内の値を効率的に管理するために使用されます。



このセクションでは、リンクカウントとコピーオンライトの概念について説明します。



値と参照のセマンティクス


PHPでは、参照の使用を明示的に要求しない限り、すべての値には常に値のセマンティクスがあります。 これは、関数に値を渡すとき、および割り当て操作中に、値の2つの異なるコピーで作業することを意味します。 これを確認するのに役立つ例がいくつかあります。

 <?php $a = 1; $b = $a; $a++; //  $a    1, $b   : var_dump($a, $b); // int(2), int(1) function inc($n) { $n++; } $c = 1; inc($c); //   $c     $n   —    var_dump($c); // int(1)
      
      





上記の例は非常に単純で明白ですが、これはあらゆる場所に適用される基本的なルールであることを理解することが重要です。 オブジェクトにも適用されます:

 <?php $obj = (object) ['value' => 1]; function fnByVal($val) { //     ,     object  integer $val = 100; } function fnByRef(&$ref) { $ref = 100; } // ,    ,   $obj,       — : fnByVal($obj); var_dump($obj); // stdClass(value => 1),  fnByVal     fnByRef($obj); var_dump($obj); // int(100)
      
      





PHP 5では、オブジェクトは参照によって自動的に渡されることがよくありますが、上記の例はそうではないことを示しています。 値が渡される関数は、渡される変数の値を変更できません。リンクが渡される関数のみがこれを行うことができます。



オブジェクトは参照渡しされたかのように動作しますが、これは事実です。 変数に異なる値を割り当てることはできませんが、オブジェクトのプロパティを変更することはできます。 これは、オブジェクトの値がオブジェクトの「実際のデータ」の検索に使用されるIDであるため可能です。 値渡しのセマンティクスでは、このIDを別のIDに変更したり、変数の型を変更したりすることはできませんが、オブジェクトの「実際のデータ」を変更することはできません。



上記の例を少し変更してみましょう。



 <?php $obj = (object) ['value' => 1]; function fnByVal($val) { //      ,       $val->value = 100; } var_dump($obj); // stdClass(value => 1) fnByVal($obj); var_dump($obj); // stdClass(value => 100),  fnByVal     
      
      





リソースについても、データの検索に使用できるIDのみが保存されるため、同じことが言えます。 したがって、値渡しのセマンティクスでは、zvalのIDまたはタイプを変更することはできませんが、リソースデータを変更することはできません(たとえば、ファイル内の位置をシフトするため)。



リンクカウントとコピーオンライト


上記の内容について少し考えると、PHPは膨大な数のコピー操作を実行する必要があるという結論に達するでしょう。 関数に変数を渡すたびに、その値をコピーする必要があります。 これは整数型または倍精度型のデータでは問題にならないかもしれませんが、1000万個の値を含む配列を関数に渡すと想像してください。 関数が呼び出されるたびに何百万もの値をコピーすると、許容できないほど遅くなります。



これを避けるために、PHPはコピーオンライトパラダイムを使用します。 Zvalは多くの変数/関数/などで共有できますが、読み取りにzvalデータが使用されている場合に限ります。 誰かがzvalデータを変更したいと思うとすぐに、変更が適用される前にコピーされます。



1つのzvalを複数の場所で使用できるため、PHPは瞬間を判断できるはずです。誰もzvalコードを使用してそれを削除しません(占有しているメモリを解放します)。 PHPはこれを単純な参照カウントにします。 ここでの「リンク」はPHP( &



指定されたリンク)ではなく、誰か(変数、関数など)がこのzvalを使用していることを示すインジケーターにすぎないことに注意してください。 このようなリンクの数はrefcount



と呼ばれ、 refcount__gc



zvalのデータメンバーに格納されます。



これがどのように機能するかを理解するために、例を見てみましょう。



 <?php $a = 1; // $a = zval_1(value=1, refcount=1) $b = $a; // $a = $b = zval_1(value=1, refcount=2) $c = $b; // $a = $b = $c = zval_1(value=1, refcount=3) $a++; // $b = $c = zval_1(value=1, refcount=2) // $a = zval_2(value=2, refcount=1) unset($b); // $c = zval_1(value=1, refcount=1) // $a = zval_2(value=2, refcount=1) unset($c); // zval_1 ,   refcount=0 // $a = zval_2(value=2, refcount=1)
      
      





ここでのロジックは単純です:リンクが追加されると、 refcount



の値が1つ増加し、リンクが削除されると、 refcount



減少します。 refcount



の値が0に達すると、zvalが削除されます。



確かに、このメソッドは循環参照の場合には機能しません。



 <?php $a = []; // $a = zval_1(value=[], refcount=1) $b = []; // $b = zval_2(value=[], refcount=1) $a[0] = $b; // $a = zval_1(value=[0 => zval_2], refcount=1) // $b = zval_2(value=[], refcount=2) // refcount zval_2    //     zval_1 $b[0] = $a; // $a = zval_1(value=[0 => zval_2], refcount=2) // $b = zval_2(value=[0 => zval_1], refcount=2) // refcount zval_1    //     zval_2 unset($a); // zval_1(value=[0 => zval_2], refcount=1) // $b = zval_2(value=[0 => zval_1], refcount=2) // refcount zval_1 ,  zval //          zval_2 unset($b); // zval_1(value=[0 => zval_2], refcount=1) // zval_2(value=[0 => zval_1], refcount=1) // refcount zval_2 ,  //          zval_1
      
      





上記のコードが起動されると、2つのzvalが変数を介してアクセスできなくなりますが、それらは相互に参照するため、メモリ内にまだ存在する状況になります。 これは、リンクカウントの問題の典型的な例です。



この問題を解決するために、PHPには別のガベージコレクションメカニズム-循環コレクターがあります。 循環コレクターは(リンクカウントメカニズムとは異なり)PHP拡張機能の開発者に対して透過的であるため、これを無視できます。 このトピックに興味がある場合は、このアルゴリズムについて説明しているPHPドキュメントを参照してください。



PHPリンクの別の機能(上記で説明したものではなく&$var



として定義されているもの)を検討する必要があります。 zvalがPHPリンクとして使用されることを示すために、 is_ref__gc



フラグがzval構造体でis_ref__gc



れます。



is_ref=1



場合、これは変更前にzvalをコピーしてはならないというシグナルであり、代わりにzval値を変更する必要があります。



 <?php $a = 1; // $a = zval_1(value=1, refcount=1, is_ref=0) $b =& $a; // $a = $b = zval_1(value=1, refcount=2, is_ref=1) $b++; // $a = $b = zval_1(value=2, refcount=2, is_ref=1) //   is_ref=1 PHP   zval //      
      
      





上記の例では、リンクを作成する前に、変数$a



refcount=1



が設定されています。 次に、複数のリンクカウントを持つ同様の例を考えてみましょう。



 <?php $a = 1; // $a = zval_1(value=1, refcount=1, is_ref=0) $b = $a; // $a = $b = zval_1(value=1, refcount=2, is_ref=0) $c = $b // $a = $b = $c = zval_1(value=1, refcount=3, is_ref=0) $d =& $c; // $a = $b = zval_1(value=1, refcount=2, is_ref=0) // $c = $d = zval_2(value=1, refcount=2, is_ref=1) // $d   $c,  **  $a and $b,  // zval    .     //  zval  is_ref=0   is_ref=1. $d++; // $a = $b = zval_1(value=1, refcount=2, is_ref=0) // $c = $d = zval_2(value=2, refcount=2, is_ref=1) //       2  zvals $d++  //  $a  $b (  ).
      
      





ご覧のとおり、 is_ref=0



へのリンクを作成するとき、c is_ref=0



およびrefcount>1



はコピーが必要です。 同様に、値渡しのコンテキストでis_ref=1



およびrefcount>1



でzvalを使用する場合、コピー操作が必要です。 このため、PHPリンクを使用すると、通常コードが遅くなります。 PHPのほとんどすべての関数は、値渡しのセマンティクスを使用するため、値is_ref=1



zvalを取得するときにコピーを作成します。



おわりに


この記事では、PHP Internals BookのZvalsの章を絞りました。 PHP開発者に役立つ資料のみを残して、拡張機能の開発に関連する多くのテキストをカットしようとしました(そうしないと、記事の量が3倍になります)。 PHPの拡張機能の開発の問題をさらに詳しく調べることに興味がある場合は、 または私の翻訳を参照してください。 現時点では、Zvalsのヘッドのみが翻訳されていますが、私は仕事を続けています。 近い将来、ハッシュテーブルとクラスに関する興味深い章を取り上げます。






[ 1 ]本の翻訳は著者の許可を得て行われていますが、非公式です。 ここで私の翻訳を読むことができます: romka.gitbooks.io/php-internals-book-ru 、ここで翻訳を助ける: github.com/romka/phpinternalsbook-ru



All Articles