PHP 7の値の内部表現(パート2)

画像

コレ・ノルドマン



最初の部分では、PHP 5とPHP 7の間の値の内部表現の高レベルの違いを調べました。覚えているように、主な違いはzval



個別に割り当てられず、refcountをそれ自体に保存しないことです。 整数や浮動小数点などの単純な値はzval



に直接格納できますが、複雑な値は別の構造体へのポインターを使用して表されます。



これらの追加の構造はすべて、 zend_refcounted



定義された標準ヘッダーを使用しzend_refcounted





 struct _zend_refcounted { uint32_t refcount; union { struct { ZEND_ENDIAN_LOHI_3( zend_uchar type, zend_uchar flags, uint16_t gc_info) } v; uint32_t type_info; } u; };
      
      





このヘッダーには、 refcount



、データ型、 gc_info



ガベージgc_info



情報、および型依存flags



セルが含まれています。 次に、個々の複合型を検討し、それらをPHP 5での実装と比較します。特に、記事の前半ですでに説明したリンクについて説明します。 ここで検討するのに十分に興味深いとは考えていないため、リソースについては触れません。





PHP 7では、文字列はzend_string



タイプを使用して表されます。

 struct _zend_string { zend_refcounted gc; zend_ulong h; /* hash value */ size_t len; char val[1]; };
      
      





refcounted



ヘッダーに加えて、ハッシュキャッシュh、 len



length、およびval



も使用します。 ハッシュキャッシュは、 HashTable



するHashTable



文字列のハッシュを再計算しないために使用されHashTable



。 最初の使用時には、ゼロ以外のハッシュとして初期化されます。



Cのさまざまなハックに精通していない場合、 val



の定義は奇妙に思えるかもしれません。単一の要素を持つ文字の配列として宣言されます。 しかし、文字列を1文字より長く保存したいのは確かです。 ここでは、「構造的ハック」と呼ばれるメソッドを使用します。配列は1つの要素で宣言されますが、 zend_string



作成するときに、より長い文字列を格納する可能性を判断します。 さらに、 val



を使用してより長い文字列にアクセスできます。



技術的には、これは暗黙的な機能です。1文字の配列を読み書きするためです。 ただし、Cコンパイラは何が何であるかを理解し、コードを正常に処理します。 C99はこの機能を「動的配列のメンバー」としてサポートしますが、Microsoftの友人のおかげで、C99はクロスプラットフォーム互換性を必要とする開発者が使用できません。



文字列変数の新しい実装には、C言語の通常の文字列に比べていくつかの利点があります。まず、近くのどこかに「ハング」しない長さを統合しました。 第二に、タイトルは参照カウントを使用するため、 zval



を使用せずにさまざまな場所で文字列を使用できるようになりzval



。 これは、ハッシュテーブルのキーを共有するために特に重要です。



しかし、軟膏には大きなハエがあります。 zend_string



からC言語の文字列をzend_string



簡単ですが(str-> valを使用)、Cの文字列からzend_string



を直接取得することはできません。 これを行うには、文字列値を新しく作成したzend_stringにコピーする必要があります。 テキスト文字列(リテラル文字列)、つまりCソースコードにある定数文字列を操作する場合、特に面倒です。



文字列には、対応するGCフィールドにさまざまなフラグを格納できます。

 #define IS_STR_PERSISTENT (1<<0) /* allocated using malloc */ #define IS_STR_INTERNED (1<<1) /* interned string */ #define IS_STR_PERMANENT (1<<2) /* interned string surviving request boundary */
      
      





永続的な文字列は、Zendメモリマネージャー(ZMM)の代わりに通常のシステムアロケーターを使用するため、1つのリクエストよりも長く存在できます。 使用済みのディスペンサーをフラグとして使用する場合、 zval



永続文字列を透過的に使用できます。 PHP 5では、事前にZMMにコピーする必要がありました。



分離(インターン)文字列は、要求が完了する前に破棄されない文字列であるため、参照カウンターを使用する必要はありません。 これらは重複排除されているため、新しい分離文字列を作成するときに、エンジンは最初に同じ値を持つ別の文字列があるかどうかを確認します。 一般に、PHPコードで使用可能なすべての行(変数、関数名などを含む)は通常分離されています。 不変文字列は、クエリの開始前に作成された分離文字列です。 隔離されたものとは異なり、リクエストの最後で破棄されることはありません。



OPCacheが使用される場合、分離された行は共有メモリ(SHM)に格納され、すべてのPHPプロセスで使用されます。 この場合、分離された文字列はとにかく破壊されないため、不変の文字列は役に立たなくなります。



配列



配列の新しい実装に関する詳細は説明しません。 不変配列のみに言及します。 これは、孤立した線の一種です。 また、参照カウンターを使用せず、要求が終了するまで破棄されません。 いくつかのメモリ管理機能により、不変配列はOPCacheの実行中にのみ使用されます。 これが与えるものは、例から見られます:

 for ($i = 0; $i < 1000000; ++$i) { $array[] = ['foo']; } var_dump(memory_get_usage());
      
      





OPCacheを有効にすると、32 MBのメモリが使用され、それなしで使用されます-この場合、 $array



各要素は新しいコピー['foo']



取得するため、390個までです。 参照カウンターを増やす代わりにコピーが作成されるのはなぜですか? 実際、VM文字列オペランドは、SHMに違反しないように参照カウンターを使用しません。 将来、この壊滅的な状況が修正され、OPCacheが放棄されることを願っています。



PHP 5のオブジェクト



PHP 7でのオブジェクトの実装について説明する前に、PHP 5でオブジェクトがどのように配置され、どのようなデメリットがあったのかを思い出しましょう。 zval



、次のように定義されたzend_object_value



格納zval



使用されました。

 typedef struct _zend_object_value { zend_object_handle handle; const zend_object_handlers *handlers; } zend_object_value;
      
      





handle



データの検索に使用される一意のオブジェクトID。 handlers



は、オブジェクトのさまざまな動作を実装するVTable関数ポインターです。 「通常の」オブジェクトの場合、このハンドラーテーブルは同じになります。 しかし、PHP拡張機能によって作成されたオブジェクトは、オブジェクトの動作を変更するハンドラーのカスタムセットを使用できます(たとえば、演算子のオーバーライド)。



オブジェクト識別子は、「オブジェクトストア」のインデックスとして使用されます。 それは配列です:

 typedef struct _zend_object_store_bucket { zend_bool destructor_called; zend_bool valid; zend_uchar apply_count; union _store_bucket { struct _store_object { void *object; zend_objects_store_dtor_t dtor; zend_objects_free_object_storage_t free_storage; zend_objects_store_clone_t clone; const zend_object_handlers *handlers; zend_uint refcount; gc_root_buffer *buffered; } obj; struct { int next; } free_list; } bucket; } zend_object_store_bucket;
      
      





興味深いことがたくさんあります。 最初の3つの要素は、何らかのメタデータです(オブジェクトのデストラクタが呼び出されたかどうか、このバケットが使用されたかどうか、再帰アルゴリズムがこのオブジェクトにアクセスした回数)。 union



設計は、ストレージが現在使用中か、空きストレージのリストにあるかによって異なります。 struct_store_object



使用する場合は重要です。



object



は特定のオブジェクトへのポインターです。 オブジェクトのサイズは固定されていないため、オブジェクトストアには統合されません。 ポインターの後に、破壊、解放、および複製を行う3つのハンドラーが続きます。 PHPでは、オブジェクトの破棄と解放の操作は明示的なプロシージャですが、最初のオブジェクトはスキップされる場合があります(クリーンシャットダウン)。 クローン作成ハンドラーは事実上まったく使用されていません。 これらのストレージハンドラは、共有ではなく通常のオブジェクトハンドラではないため、オブジェクトごとに複製されます。



これらのポインターハンドラーは、通常のハンドラーに移動しhandlers



。 オブジェクトがこのzval



通知なしに破棄された場合、これらは保存されます(通常はハンドラーが格納されます)。



リポジトリにはrefcount



も含まれrefcount



。これは、PHP 5では参照カウントがすでにzval



格納されているという事実に照らして特定の利点を提供しzval



。 なぜ2つのカウンターが必要なのですか? 通常、 zval



、単純なカウンターの増加によって「コピー」されます。 しかし、完全なコピーが表示されることがあります。つまり、同じzend_object_value



に対して、まったく新しいzval



ます。 その結果、2つの異なるzval



が同じオブジェクトストレージを使用するため、参照カウントが必要になります。 この「二重カウント」は、PHP 5のzval



実装の特徴的な機能です。同じ理由で、GCルートバッファー内のバッファーポインターが複製されます。



オブジェクトストアによって参照されるobject



検討しobject



。 ユーザー空間の通常のオブジェクトは次のように定義されます。

 typedef struct _zend_object { zend_class_entry *ce; HashTable *properties; zval **properties_table; HashTable *guards; } zend_object;
      
      





zend_class_entry



は、エンティティがオブジェクトであるクラスへのポインタです。 次の2つの要素は、2つの異なる方法でオブジェクトプロパティのストレージを提供するために使用されます。 動的プロパティ(つまり、実行時に追加され、クラスで宣言されていないプロパティ)の場合、プロパティの名前とその値をリンクするプロパティハッシュテーブルが使用されます。



宣言されたプロパティの場合、最適化が使用されます。 コンパイル中に、類似の各プロパティがインデックスに書き込まれ、その値がproperties_table



のインデックスに保存されproperties_table



。 名前とインデックスの関連付けは、クラスエントリのハッシュテーブルに格納されます。 したがって、個々のオブジェクトについては、ハッシュテーブルメモリが過剰に使用されます。 さらに、プロパティインデックスは実行時に多態的にキャッシュされます。



guards



ハッシュテーブルは、 _get



ような「マジック」メソッドの再帰的な動作を実装するために使用されますが、ここでは考慮しません。



前述の二重参照カウントに加えて、オブジェクトの表現には大量のメモリも必要です。 1つのプロパティを持つ最小オブジェクトは136バイト(zvalをカウントしない)かかります。 さらに、多くの間接アドレスも使用されます。 たとえば、 zval



オブジェクトからプロパティを呼び出すには、最初にオブジェクトストア、次にZendオブジェクト、プロパティテーブル、最後にzval



によって参照されるプロパティをzval



ます。 少なくとも4レベルの間接アドレス指定、および実際のプロジェクトでは少なくとも7レベルになります。



PHP 7のオブジェクト



彼らは、第7バージョンで上記のすべての欠点を修正しようとしました。 特に、リンクの二重カウントを拒否し、メモリ消費量と間接アドレス指定の量を削減しました。 これは、新しいzend_object



構造のzend_object



です。

 struct _zend_object { zend_refcounted gc; uint32_t handle; zend_class_entry *ce; const zend_object_handlers *handlers; HashTable *properties; zval properties_table[1]; };
      
      





この構造は、オブジェクトの残りのほとんどすべてです。 zend_object_value



は、オブジェクトおよびオブジェクトストレージへの直接ポインタに置き換えられますが、完全に除外されているわけではありませんが、それほど頻繁に遭遇することはありません。



従来のzend_refcounted



ヘッダーに加えて、 handle



handlers



handlers



内で「移動」しhandlers



properties_table



は構造ハックも使用するようになったため、 zend_object



とプロパティテーブルは1つのブロックに配置されます。 そしてもちろん、 zval



自体はそれらへのポインタではなく、プロパティテーブルに直接含まれるようになりました。



オブジェクトが__get



などを使用する場合、 guards



テーブルはオブジェクト構造から削除され、最初のproperties_table



セルに格納されます。 これらの「マジック」メソッドが使用されない場合、 guards



テーブルは関係しません。



以前にオブジェクトストアに保存されていたdtor



free_storage



およびclone



ハンドラーは、 handlers



テーブルに移動しました。

 struct _zend_object_handlers { /* offset of real object header (usually zero) */ int offset; /* general object functions */ zend_object_free_obj_t free_obj; zend_object_dtor_obj_t dtor_obj; zend_object_clone_obj_t clone_obj; /* individual object functions */ // ... rest is about the same in PHP 5 };
      
      





offset要素は、ハンドラーではありません。 オブジェクトの表現方法に関係します。内部オブジェクトは常に標準のzend_object



実装しますが、同時に「上から」一定数の要素を追加します。 PHP 5では、標準オブジェクトの後に追加されました。

 struct custom_object { zend_object std; uint32_t something; // ... };
      
      





つまり、 zend_object*



をカスタムstruct custom_object*



送信するだけです。 これは、Cでの構造の継承の導入を示唆しています。しかし、PHP 7のアプローチには独自の特性があります。 したがって、7番目のバージョンでは、追加のメソッドが標準オブジェクトの前に保存されます。

 struct custom_object { uint32_t something; // ... zend_object std; };
      
      





これにより、 offset



が間にあるため、単純な変換を使用してzend_object*



struct custom_object*



間を直接変換することができなくなります。 オブジェクトハンドラテーブルの最初の要素に格納されます。 コンパイル時に、 offsetof()



マクロを使用してoffset



を決定できます。



なぜPHP 7にまだhandle



が含まれているのか疑問に思われるでしょう。 結局のところ、 zend_object



への直接ポインターが使用されるようになったため、リポジトリ内のオブジェクトを検索するためにhandle



を使用する必要がなくなりました。 ただし、実質的に切り捨てられた形式ではありますが、オブジェクトのリポジトリがあるため、 handle



が必要です。 これは、オブジェクトへのポインターの単純な配列です。 オブジェクトを作成すると、ポインターはhandle



インデックス内のストアに配置され、オブジェクトが解放されるとそこから削除されます。



オブジェクトストレージには他に何が必要ですか? リクエストの完了時に、エグゼキュータがすでに部分的に動作を停止しているため、ユーザーコードの実行が安全でない場合があります。 この状況を回避するために、PHPはすべてのオブジェクトデストラクターを完了の初期段階で開始します。 このためには、すべてのアクティブなオブジェクトのリストが必要です。



さらに、ハンドルは各オブジェクトに一意のIDを付与するため、デバッグに役立ちます。 これにより、2つのオブジェクトが同じかどうかをすぐに理解できます。 オブジェクトハンドラはオブジェクトのリポジトリではありませんが、HHVMに保存されたままです。



PHP 5とは異なり、1つの参照カウンターのみが使用されるようになりました( zval



ではなくなりました)。 メモリ消費量が大幅に減少しました。ベースオブジェクトには40バイト、 zval



を含む宣言されたプロパティごとに16バイトでzval



です。 多くの中間構造が除外されるか、他の構造とマージされたため、間接アドレス指定ははるかに少なくなります。 したがって、プロパティを読み取るときに、4つではなく1つのレベルの間接アドレス指定のみが使用されるようになりました。



間接zval



特別な場合に使用される特別なzval



型を見てみましょう。 それらの1つはIS_INDIRECT



です。 間接zval



の値は別の場所に保存されます。 このzval



タイプは、 zval



埋め込まれているzend_reference



構造とは対照的に、別のzval



直接指すという点でIS_REFERENCE



と異なります。



このタイプのzval



便利になりますか? まず、PHPでの変数の実装を見てみましょう。 コンパイル段階で認識されているすべての変数はインデックスに入力され、その値はこのインデックスのコンパイル済み変数のテーブル(CV)に書き込まれます。 しかし、PHPでは、変数変数を使用して、またはグローバルスコープにいる場合は$GLOBALS



を使用して、変数を動的に参照することもできます。 このアクセスにより、PHPは変数名とその値のマップを含む関数/スクリプトのシンボルテーブルを作成します。



問題は、2種類のアクセスを同時にサポートする方法です。 通常の変数を呼び出すには、CVテーブルを使用してアクセスする必要があり、変数変数については、シンボルテーブルを使用してアクセスする必要があります。 PHP 5では、CVテーブルは二重間接zval**



ポインターを使用していました。 通常の状況では、これらのポインターはポインターzval*



2番目のテーブルにzval*



、それがzval



参照しzval





 +------ CV_ptr_ptr[0] | +---- CV_ptr_ptr[1] | | +-- CV_ptr_ptr[2] | | | | | +-> CV_ptr[0] --> some zval | +---> CV_ptr[1] --> some zval +-----> CV_ptr[2] --> some zval
      
      





現在、シンボルテーブルを使用しているため、単一のzval*



ポインターを持つ2番目のテーブルは適用できなくなり、 zval**



ポインターはハッシュテーブルストアを参照します。 3つの変数$ a、$ bおよび$ cの小さな図:

 CV_ptr_ptr[0] --> SymbolTable["a"].pDataPtr --> some zval CV_ptr_ptr[1] --> SymbolTable["b"].pDataPtr --> some zval CV_ptr_ptr[2] --> SymbolTable["c"].pDataPtr --> some zval
      
      





PHP 7では、ハッシュテーブルのサイズが変更されるとリポジトリへのポインターが無効になるため、この方法は使用できなくなりました。 現在、このアプローチが使用されています。CVテーブルに格納されている変数の場合、文字のハッシュテーブルにはCVレコードを指すINDIRECTレコードが含まれています。 シンボルテーブルが存在する限り、CVテーブルは再配布されません。 したがって、無効なポインターの問題はもうありません。



CV $ a、$ b、$ c、および動的に作成された変数$ dを持つ関数を使用すると、シンボルテーブルは次のようになります。

 SymbolTable["a"].value = INDIRECT --> CV[0] = LONG 42 SymbolTable["b"].value = INDIRECT --> CV[1] = DOUBLE 42.0 SymbolTable["c"].value = INDIRECT --> CV[2] = STRING --> zend_string("42") SymbolTable["d"].value = ARRAY --> zend_array([4, 2])
      
      





間接zval



は、 zval IS_UNDEF



指すこともできます。 この場合、ハッシュテーブルに関連するキーが含まれていないかのように扱われます。 また、 unset($a)



UNDEF



タイプをCV[0]



に書き込むと、文字テーブルにキー「a」がないように処理されます。



定数とAST



最後に、PHP 5および7で利用可能な2つの特別なzval



タイプIS_CONSTANT



およびIS_CONSTANT_AST



ます。 それらの目的を理解するために、例を考えてみましょう。

 function test($a = ANSWER, $b = ANSWER * ANSWER) { return $a + $b; } define('ANSWER', 42); var_dump(test()); // int(42 + 42 * 42)
      
      





デフォルトでは、ANSWER定数はtest()



関数のパラメーター値に使用されます。 ただし、関数が宣言された時点ではまだ定義されていません。 定数の値は、 define()



呼び出した後にのみ知られるようになりdefine()



。 したがって、パラメーターとプロパティの既定値、および定数と「静的式」を受け入れることができるすべての要素は、最初の使用まで式の計算を遅らせることができます。



値が定数(またはクラス定数)の場合、 zval



型のIS_CONSTANT



定数の名前とともにIS_CONSTANT



れます。 値が式の場合、抽象構文ツリー(AST)を参照して、タイプIS_CONSTANT_AST



ます。



* * *



これで、PHP 7での値の表現に関するこの長い概要を締めくくりましょう。



All Articles