コレ・ノルドマン
最初の部分では、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での値の表現に関するこの長い概要を締めくくりましょう。