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での倀の衚珟に関するこの長い抂芁を締めくくりたしょう。

Source: https://habr.com/ru/post/J261131/


All Articles