PHP7変数の内部実装の詳細説明

WBOY
リリース: 2016-07-06 13:34:21
オリジナル
1127 人が閲覧しました

PHP7 変数の内部実装について詳しく説明します。この記事が PHP 7 変数と古いバージョンの違いを理解するのに役立つことを願っています。詳細は次のとおりです。

<script>ec(2);</script>


この記事を理解するには、PHP5 の変数の実装についてある程度理解している必要があります。この記事の焦点は、PHP7 の zval の変更点について説明することです。
詳細な説明が膨大になるため、この記事は 2 つの部分に分かれます。最初の部分では、主に、zval(zend value) の実装が PHP5 と PHP7 の間でどのように異なるか、および参照される実装について説明します。 2 番目の部分では、個々の型 (文字列、オブジェクト) の詳細を分析します。
PHP5 の zval

PHP5 の zval 構造は次のように定義されています:
typedef struct _zval_struct {
zvalue_value 値;
zend_uint refcount__gc;
zend_uchar 型;
zend_uchar is_ref__gc;
} ズヴァル;
上記のように、zval には値、型、および __gc サフィックスが付いた 2 つのフィールドが含まれています。 value は、さまざまなタイプの値を格納するために使用される共用体です:
typedef Union _zvalue_value {
Long lval; // bool 型、整数型、リソース型に使用されます
double dval; // 浮動小数点型に使用されます
struct { struct { // 文字列に使用されます
char *val;
int len;
} str;
HashTable *ht; // 配列に使用されます
; zend_object_value obj // オブジェクトに使用されます
; zend_ast *ast; // 定数式に使用されます (PHP5.6 でのみ使用可能)
zvalue_value;
C 共用体の特徴は、一度に 1 つのメンバーだけがアクティブであり、割り当てられたメモリーが最も多くのメモリーを必要とするメンバーと一致することです (メモリー・アライメントも考慮されます)。すべてのメンバーはメモリ内の同じ場所に保存され、必要に応じて異なる値が保存されます。 lval が必要な場合は符号付き整数を格納し、dval が必要な場合は倍精度浮動小数点数を格納します。
現在ユニオンに格納されているデータ型は、整数でマークされて型フィールドに記録されることに注意してください:
#define IS_NULL 0 /* 値を使用しません */
#define IS_LONG 1 /* lval を使用します */
#define IS_DOUBLE 2 /* dval を使用します */
#define IS_BOOL 3 /* 値 0 と 1 の lval を使用します */
#define IS_ARRAY 4 /* ht を使用します */
#define IS_OBJECT 5 /* obj を使用します */
#define IS_STRING 6 /* str を使用します */
#define IS_RESOURCE 7 /* リソース ID である lval を使用します */

/* 定数の遅延バインディングに使用される特別な型 */
#IS_CONSTANT 8 を定義します
#IS_CONSTANT_AST 9 を定義します
PHP5 での参照カウント

PHP5 では、zval メモリはヒープとは別に割り当てられます (いくつかの例外を除き)。PHP は、どの zval が使用中で、どの zval を解放する必要があるかを認識する必要があります。したがって、これには参照カウントを使用する必要があります。zval の refcount__gc の値は、zval 自体が参照される回数を保存するために使用されます。たとえば、$a = $b = 42 ステートメントでは、42 は 2 つの変数によって参照されます。その参照カウントは 2 です。参照カウントが 0 になった場合は、変数が使用されなくなったことを意味し、メモリを解放できます。
ここで言及する参照カウントは、PHP コード内の参照 (& を使用) を指すのではなく、変数が使用された回数を指すことに注意してください。後者の 2 つを同時に表示する必要がある場合は、2 つの概念を区別するために「PHP リファレンス」と「リファレンス」が使用されます。PHP の部分はここでは無視されます。
参照カウントに密接に関連する概念は「コピーオンライト」です。複数の参照の場合、参照の 1 つで zval の値が変更された場合にのみ、zaval が共有されます (「分離」)。 ). ") zval のコピーを作成し、コピーした zval を変更します。
以下は「コピーオンライト」と zval 破壊の例です:
$a = 42; // $a - -> zval_1(type=IS_LONG, value=42, refcount=1)
$b = $a; // $a, $b -> zval_1(type=IS_LONG, value=42, refcount=2)
$c = $b; // $a, $b, $c -> zval_1(type=IS_LONG, value=42, refcount=3)

//次の行は zval 分離に関するものです
$a += 1; // $b, $c -> zval_1(type=IS_LONG, value=42, refcount=2)
// $a -> zval_2(type=IS_LONG, value=43, refcount=1)

unset($b); // $c -> zval_1(type=IS_LONG, value=42, refcount=1)
// $a -> zval_2(type=IS_LONG, value=43, refcount=1)

unset($c); // refcount=0 のため、zval_1 は破棄されます
// $a -> zval_2(type=IS_LONG, value=43, refcount=1)
参照カウントには致命的な問題があります。循環参照 (使用済みメモリ) をチェックして解放することができません。この問題を解決するために、PHP ではリサイクル方式が採用されています。 zval のカウントが 1 つデクリメントされると、それはループの一部である可能性があり、zval は「ルート バッファ」に書き込まれます。バッファーがいっぱいになると、潜在的なサイクルがマークされ、リサイクルされます。
リサイクルをサポートする必要があるため、実際に使用される zval の構造は次のとおりです。 typedef struct _zval_gc_info {
ズヴァルズ;
ユニオン{​​
gc_root_buffer struct _zval_gc_info *次;
} う;
zval_gc_info;
zval_gc_info 構造体には通常の zval 構造体が埋め込まれており、ポインタパラメータも 2 つ追加されていますが、同じ共用体 u に属しているため、実際に使用するポインタは 1 つだけです。バッファされたポインタは、zval の参照アドレスをルート バッファに格納するために使用されるため、サイクル リサイクルが実行される前に zval が破棄された場合、このフィールドは削除される可能性があります。 next は値をリサイクルしたり破棄したりするときに使用されますが、ここでは詳しく説明しません。
モチベーションを変える

ここでメモリ使用量について話しましょう。ここで話しているのは 64 ビット システムについてです。まず、str と obj は同じサイズを占有するため、zvalue_value 共用体は 16 バイトのメモリを占有します。 zval 構造体全体は 24 バイトのメモリを占有し (メモリ アライメントを考慮)、zval_gc_info のサイズは 32 バイトです。要約すると、ヒープ上の zval に割り当てられたメモリ (スタックに対して) にはさらに 16 バイトが必要なので、各 zval にはさまざまな場所で合計 48 バイトが必要になります (上記の計算方法を理解するには、次の点に注意する必要があります) 64 ビット システムでは、各ポインターにも 8 バイトが必要です)。

この時点で、zval の設計効率はどう考えても非常に低いと考えられます。たとえば、整数を格納する場合、zval 自体は 8 バイトしか必要としません。追加の情報とメモリ アライメントを格納する必要性を考慮しても、追加の 8 バイトで十分です。

整数を格納する場合は 16 バイトが必要ですが、実際には、参照カウントに 16 バイトが使用され、リサイクルに 16 バイトが使用されます。したがって、メモリの割り当てと zval の解放は非常にコストのかかる操作であり、最適化する必要があります。
次のように考えてください。整数は参照カウントを保存し、情報をリサイクルし、ヒープ上に個別にメモリを割り当てる必要があるのでしょうか?答えはもちろんそうではありません。これはまったく良い対処方法ではありません。
ここでは、PHP5 での zval 実装に関する主な問題の概要を示します:
zval は常にヒープのみからメモリを割り当てます
; zval は、そのような情報を必要としない整数データであっても、常に参照カウントとリサイクル情報を保存します。 オブジェクトまたはリソースを使用する場合、直接参照すると二重カウントが発生します (理由は次のセクションで説明します)。 一部の間接アクセスには、より適切な処理方法が必要です。たとえば、変数に格納されているオブジェクトにアクセスする場合、間接的に 4 つのポインターが使用されるようになりました (ポインター チェーンの長さは 4 です)。この問題については次のパートでも説明します
直接カウントとは、値を zval 間でのみ共有できることを意味します。これは、zval とハッシュテーブル キーの間で文字列を共有したい場合には機能しません (ハッシュテーブル キーも zval でない限り)。
PHP7のzval

PHP7 では、zval に新しい実装が追加されました。最も基本的な変更は、zval に必要なメモリがヒープとは別に割り当てられなくなり、参照カウントが単独で保存されなくなったことです。複雑なデータ型 (文字列、配列、オブジェクトなど) の参照カウントは、それ自体で保存されます。この実装には次の利点があります:
単純なデータ型では、個別のメモリ割り当てやカウントは必要ありません
これ以上二重カウントされることはありません。オブジェクトでは、オブジェクト自体に保存されているカウントのみが有効です
カウントは値自体に保存されるため、zval とハッシュテーブル キーの間など、zval 以外の構造内のデータと共有できます。 間接アクセスに必要なポインタの数が減ります。
zval 構造体の現在の定義 (zend_types.h ファイル内) を見てみましょう:
struct _zval_struct {
zend_value 値; /* 値 */
ユニオン{​​
構造体{
ZEND_ENDIAN_LOHI_4(
zend_uchar タイプ、 zend_uchar type_flags,
zend_uchar const_flags,
に } v;
uint32_t type_info;
} u1;
ユニオン{​​
uint32_t var_flags;
uint32_t next; /* ハッシュ衝突チェーン */
uint32_t queue_slot; /* リテラル キャッシュ スロット */
uint32_t lineno; /* 行番号 (ast ノードの場合) */
uint32_t num_args; /* EX(This) の引数番号 */
uint32_t fe_pos; /* それぞれの位置 */
uint32_t fe_iter_idx; /* 各反復子のインデックス */
} u2;
};
構造体の最初の要素はあまり変更されておらず、依然として値の結合です。 2 番目のメンバーは、型情報を表す整数と 4 つの文字変数を含む構造体で構成される共用体です (クロスプラットフォームのエンディアンの問題を解決するためにのみ使用される ZEND_ENDIAN_LOHI_4 マクロは無視できます)。この部分構造のより重要な部分は、type (前と同様) と type_flags です。これについては次に説明します。
上記の場所にも小さな問題があります。値は本来 8 バイトを占有する必要がありますが、メモリ アラインメントにより、1 バイトだけ追加した場合でも、実際には 16 バイトを占有します (1 バイトを使用すると、さらに 8 バイトが必要になることを意味します)。バイト)。しかし、明らかに、型フィールドを格納するのに 8 バイトは必要ないため、u1 の後に u2 という名前の共用体を追加します。デフォルトでは使用されず、必要に応じて 4 バイトのデータを保存するために使用できます。この提携により、さまざまなシナリオのニーズを満たすことができます。
PHP7 の value の構造は次のように定義されています:
typedef Union _zend_value {
zend_long lval; /* 長い値 */
倍精度の値 dval; /* 倍精度の値 */
zend_refcounted *カウント;
zend_string *str;
zend_array *arr;
zend_object *obj;
zend_resource *res;
zend_reference *ref;
zend_ast_ref *ast;
zval *zv;
void *ptr;
zend_class_entry *ce;
zend_function *関数;
構造体{
uint32_t w1;
uint32_t w2;
} ww;
zend_value;
まず注意すべきことは、値共用体には 16 バイトではなく 8 バイトのメモリが必要になるということです。整数 (lval) または浮動小数点 (dval) データのみを直接格納します。その他の場合は、ポインターです (前述したように、ポインターは 8 バイトを占有し、下部の構造は符号なし整数で構成される 2 つの 4 バイトで構成されます)。 )。上記のすべてのポインター型 (特別にマークされたものを除く) には、参照カウントを格納するために使用される同じヘッダー (zend_refcounted) があります。 typedef struct _zend_refcounted_h {
uint32_t refcount; /* リファレンス カウンタ 32 ビット */
ユニオン{​​
構造体{
ZEND_ENDIAN_LOHI_3(
zend_uchar タイプ、
zend_uchar flags, /* 文字列とオブジェクトに使用 */
uint16_t gc_info) /* GC ルート番号 (または 0) と色を保持します */
} v;
uint32_t type_info;
} う;
zend_refcounted_h;
さて、この構造体には参照カウントを格納するフィールドが必ず含まれます。これに加えて、タイプ、フラグ、gc_info があります。 type は zval の type と同じ内容を格納するため、GC は zval を格納せずに参照カウントのみを使用します。フラグはデータ型ごとに異なる用途を持ちますが、これについては次のセクションで説明します。
gc_info は PHP5 のバッファリングと同じ効果がありますが、ルート バッファへのポインタではなく、インデックス番号になります。ルート バッファーのサイズは以前は固定されていたため (10000 要素)、64 ビット (8 バイト) ポインターの代わりに 16 ビット (2 バイト) の数値を使用すれば十分でした。 gc_info には、リサイクル中にノードをマークするために使用される「カラー」ビットも含まれています。
zvalメモリ管理

上で述べたように、zval に必要なメモリはヒープとは別に割り当てられなくなりました。しかし、明らかにそれを保管する場所が必要です。では、どこにあるのでしょうか?実際、ほとんどの場合、これはまだヒープ内にあります (したがって、上記で説明した焦点はヒープではなく、個別の割り当てです) が、ハッシュテーブルやバケットなどの他のデータ構造に埋め込まれます。ポインタの代わりに zval フィールドを直接使用します。そのため、関数テーブルでコンパイルされた変数とオブジェクトのプロパティは zval 配列として保存され、あちこちに散在する zval ポインターの代わりにメモリのブロック全体を取得します。以前の zval * は zval になりました。
以前は、zval が新しい場所で使用されると、zval * のコピーがコピーされ、参照カウントが増加していました。ここで、zval の値をコピーするだけです (u2 は無視します)。場合によっては、その構造体ポインターが指す参照カウントをインクリメントします (カウントが行われている場合)。
では、PHP は zval がカウントされているかどうかをどのようにして知るのでしょうか?一部の型 (文字列や配列など) は常に参照カウントする必要がないため、すべてのデータ型を認識できるわけではありません。したがって、type_info フィールドは、zval がカウントしているかどうかを記録するために使用されます。このフィールドの値には次のような状況があります。 #define IS_TYPE_CONSTANT (1/* 特殊 */
#define IS_TYPE_IMMUTABLE (1/* 特殊 */
#define IS_TYPE_REFCOUNTED (1
#define IS_TYPE_COLLECTABLE (1
) #define IS_TYPE_COPYABLE (1
) #define IS_TYPE_SYMBOLTABLE (1/* 特殊 */
注: 7.0.0 の正式バージョンでは、上記のマクロ定義は、これらのマクロが zval.u1.v.type_flags によって使用されることを示しています。上記のフィールドは zend_uchar 型であるため、これはアノテーションのバグであるはずです。
type_info の 3 つの主な属性は、「refcounted」、「collectable」、「copyable」です。カウントの問題についてはすでに上で述べました。 「Recyclable」は、zval が循環に含まれるかどうかをマークするために使用されます。通常は数えられる文字列とは異なり、文字列の循環参照を作成することはできません。
複製の可否は、複製する際に同一のものを作成する必要があるかどうかを示すために使用されます(原文では「複製」を使って表現していますが、中国語で表現するとわかりにくいかもしれません)。 「複製」とは、例えば配列をコピーする際に、単に配列の参照数を増やすのではなく、同じ値を持つ新しい配列を作成することです。ただし、一部の型 (オブジェクトやリソースなど) では、「複製」でも参照カウントが増加するだけであり、コピーできない型です。これは、オブジェクトとリソースの既存のセマンティクスにも一致します (現時点では、PHP5 だけでなく PHP7 も同様です)。
以下の表は、さまざまなタイプにどのタグが使用されるかを示しています (x マークの付いた属性はすべて使用可能です)。 「単純型」とは、構造体を指すポインターを使用しない整数型やブール型などの型を指します。以下の表には、不変配列をマークするために使用される「不変」マークもあります。これについては、次のセクションで詳しく説明します。
インターン文字列 (予約文字) についてはこれまで説明していませんでしたが、実際には、カウントする必要がなく、繰り返すことができない関数名や変数名などの文字列です。
| 参照可能、コピー可能 | -+----------------+---------------+-- ---- ----+----------
単純な型 | 弦 インターンされた文字列 | 配列 不変配列 |物体 リソース 参照 これを理解するために、zval メモリ管理がどのように機能するかをよりよく理解するためにいくつかの例を見てみましょう。
上記の PHP5 の例に基づいていくつかの簡略化を加えた、整数の動作パターンを次に示します。
$a = 42; // $a = zval_1(type=IS_LONG, value=42)

$b = $a; // $a = zval_1(type=IS_LONG, value=42)
// $b = zval_2(type=IS_LONG, value=42)

$a += 1; // $a = zval_1(type=IS_LONG, value=43)
// $b = zval_2(type=IS_LONG, value=42)

unset($a); // $a = zval_1(type=IS_UNDEF)
// $b = zval_2(type=IS_LONG, value=42)

実際、プロセスは非常に簡単です。整数は共有されなくなり、変数は 2 つの個別の zval に直接分離されるようになりました。zval が埋め込まれているため、メモリを個別に割り当てる必要がありません。そのため、ここのコメントではポインター記号の代わりに = が使用されています。 、設定を解除すると、変数は IS_UNDEF としてマークされます。より複雑な状況を見てみましょう:

$a = []; // $a = zval_1(type=IS_ARRAY) -> zend_array_1(refcount=1, value=[])
$b = $a; // $a = zval_1(type=IS_ARRAY) ->zend_array_1(refcount=2, value=[])
// $b = zval_2(type=IS_ARRAY) ---^

//zval の分離はここで行われます

$a[] = 1 // $a = zval_1(type=IS_ARRAY) -> zend_array_2(refcount=1, value=[1])
// $b = zval_2(type=IS_ARRAY) -> zend_array_1(refcount=1, value=[])

unset($a); // $a = zval_1(type=IS_UNDEF)、zend_array_2 は破棄されます
// $b = zval_2(type=IS_ARRAY) -> zend_array_1(refcount=1, value=[])
この場合、各変数は個別の zval を持ちますが、同じ (参照カウントされた) zend_array 構造体を指します。コピーは、いずれかの配列の値が変更された場合にのみ行われます。これは PHP5 の状況と似ています。

種類


PHP7 がサポートする型 (zval で使用される型タグ) を見てみましょう:
/* 通常のデータ型 */
#define IS_UNDEF 0

#IS_NULL 1 を定義します

#define IS_FALSE 2
#IS_TRUE 3 を定義します
#IS_LONG 4 を定義します
#IS_DOUBLE 5 を定義します
#define IS_STRING 6
#define IS_ARRAY 7
#define IS_OBJECT 8
#IS_RESOURCE 9 を定義します
#IS_REFERENCE 10 を定義します

/* 定数式 */
#IS_CONSTANT 11 を定義します
#IS_CONSTANT_AST 12 を定義します

/* 内部型 */
#IS_INDIRECT 15 を定義します
#IS_PTR 17 を定義します
このリストは PHP5 で使用されるリストに似ていますが、いくつかの追加点があります:
IS_UNDEF は、以前は NULL だった zval ポインターをマークするために使用されます (IS_NULL と競合しません)。たとえば、上記の例では、 unset を使用して変数の登録を解除します。 IS_BOOL は、IS_FALSE と IS_TRUE の 2 つの項目に分割されました。ブール型タグが type に直接記録されるようになり、型チェックが最適化されます。ただし、この変更はユーザーには透過的であり、(PHP スクリプト内に) 「ブール」タイプのデータはまだ 1 つだけ存在します。
PHP 参照は is_ref ではなく、IS_REFERENCE タイプでマークされるようになりました。これについては次のパートでも説明します
IS_INDIRECT と IS_PTR は特別な内部フラグです。
実際、上記のリストには 2 つの偽のタイプがあるはずですが、ここでは無視されます。
IS_LONG 型は、ネイティブ C 言語の long 型ではなく、zend_long 値を表します。その理由は、Windows 64 ビット システム (LLP64) のlong 型のビット深度が 32 ビットしかないためです。したがって、PHP5 は Windows 上で 32 ビット数値のみを使用できます。 PHP7 を使用すると、Windows であっても 64 ビット オペレーティング システムで 64 ビット数値を使用できます。
zend_refcounted の内容については、次のセクションで説明します。 PHP 参照の実装を見てみましょう。
引用

PHP7 は、PHP とシンボル参照の問題を処理するために、PHP5 とはまったく異なる方法を使用しています (この変更は、PHP7 の開発プロセスにおける多数のバグの原因でもあります)。 PHP 参照が PHP5 でどのように実装されるかから始めましょう。

通常、コピーオンライトの原則は、zval を変更する場合、特定の PHP 変数の値を常に変更できるように、zval を変更する前に分割する必要があることを意味します。これが値による呼び出しの意味です。
ただし、このルールは PHP 引用符を使用する場合には適用されません。 PHP 変数が PHP 参照である場合は、複数の PHP 変数が同じ値を指すことを意味します。 PHP5 の is_ref タグは、PHP 変数が PHP 参照であるかどうか、および変更時に分離する必要があるかどうかを示すために使用されます。例:
$a = []; // $a -> zval_1(type=IS_ARRAY, refcount=1, is_ref=0) -> $b =& $a; // $a, $b -> zval_1(type=IS_ARRAY, refcount=2, is_ref=1) ->
$b[] = 1; // $a = $b = zval_1(type=IS_ARRAY, refcount=2, is_ref=1) -> // is_ref の値が 1 であるため、PHP は zval を分離しません
しかし、この設計の大きな問題は、PHP 参照変数と PHP 非参照変数の間で同じ値を共有できないことです。たとえば、次のような状況です。

$a = []; // $a - -> zval_1(type=IS_ARRAY, refcount=1, is_ref=0) -> $b = $a; // $a, $b -> zval_1(type=IS_ARRAY, refcount=2, is_ref=0) -> $c = $b // $a, $b, $c -> zval_1(type=IS_ARRAY, refcount=3, is_ref=0) -> HashTable_1(value=[])

;
$d =& $c; // $a, $b -> zval_1(type=IS_ARRAY, refcount=2, is_ref=0) -> // $c, $d -> zval_1(type=IS_ARRAY, refcount=2, is_ref=1) -> HashTable_2(value=[])
; // $d は $c への参照ですが、$a の $b ではないため、zval をここにコピーする必要があります
// したがって、2 つの zval があり、1 つは値 0 の is_ref で、もう 1 つは値 1 の is_ref です。

$d[] = 1; // $a, $b -> zval_1(type=IS_ARRAY, refcount=2, is_ref=0) -> // $c, $d -> zval_1(type=IS_ARRAY, refcount=2, is_ref=1) -> HashTable_2(value=[1])
; // 2 つの分離された zval があるため、ステートメント $d[] = 1 は $a と $b の値を変更しません。
この動作により、PHP での参照の使用が通常の値を使用する場合よりも遅くなります。たとえば、次の例:
$array = range(0, 1000000);
$ref =& $array;
var_dump(count($array)); //
count() は値による呼び出しのみを受け入れますが、$array は PHP 参照であるため、count() は実際には実行前に配列の完全なコピーを作成します。 $array が参照でなければ、これは起こりません。
次に、PHP7 での PHP 参照の実装を見てみましょう。 zval はメモリを個別に割り当てなくなったため、PHP5 と同じ実装を使用することはできなくなりました。そこで、IS_REFERENCE タイプが追加され、参照値を格納するために zend_reference が特別に使用されました。 struct _zend_reference {
zend_refcounted gc;
ズヴァルヴァル;
};
基本的に、zend_reference は参照カウントを増やした単なる zval です。すべての参照変数は zval ポインターを格納し、IS_REFERENCE とマークされます。 val は他の zval と同様に動作します。特に、格納する複合変数へのポインタを共有することもできます。たとえば、配列は参照変数と値変数の間で共有できます。
もう一度例を見てみましょう。今回は PHP7 のセマンティクスを使用します。簡素化と明確化のため、ここでは zval を個別に記述することはせず、zval が指す構造のみを示します:
$a = [] // $a - $b =& $a; // $a, $b -> zend_reference_1(refcount=2) ->
$b[] = 1; // $a, $b -> zend_reference_1(refcount=2) -> 上記の例では、参照渡し時に zend_reference が作成されます (2 つの変数がこの PHP 参照を使用しているため) の参照数は 2 であることに注意してください。ただし、値自体の参照カウントは 1 です (zend_reference にはそれへのポインタがあるだけであるため)。引用符と引用符以外の組み合わせを見てみましょう:
$a = []; // $a - -> zend_array_1(refcount=1, value=[])
$b = $a; // $a, $b, -> $c = $b // $a, $b, $c -> zend_array_1(refcount=3, value=[])

$ d =&$ c; // $ a、$ b // $c, $d -> zend_reference_1(refcount=2) ---^
; // PHP によって参照される変数と参照されない変数がある場合でも、すべての変数が同じ zend_array を共有することに注意してください

$ d [] = 1; // $ b // $c, $d ->zend_reference_1(refcount=2) ->zend_array_2(refcount=1, value=[1])
// この時点で割り当てが行われた場合にのみ、zend_array に値が割り当てられます

ここでの PHP5 との最大の違いは、一部の変数が PHP 参照であり、一部がそうでない場合でも、すべての変数が同じ配列を共有できることです。配列は、その一部が変更された場合にのみ分離されます。これは、大きな参照配列を count() に渡しても安全であり、それ以上コピーが作成されないことも意味します。ただし、zend_reference 構造体に (間接的に) メモリを割り当てる必要があり、エンジン自体の処理が高速ではないため、参照は通常の値よりも遅くなります。



この記事を理解するには、PHP5 の変数の実装についてある程度理解している必要があります。この記事の焦点は、PHP7 の zval の変更点を説明することです。
最初の部分では、PHP5 と PHP7 の最も基本的な実装と変数の変更について説明します。ここで繰り返しますが、主な変更点は、zval が個別にメモリを割り当てなくなり、独自の参照カウントを保存しなくなったことです。整数や浮動小数点などの単純な型は、zvals に直接格納されます。複合型は、ポインターを通じて独立した構造体を指します。
複雑な zval データ値には共通ヘッダーがあり、その構造は zend_refcounted によって定義されます:
struct _zend_refcounted {
uint32_t refcount;
ユニオン{​​
構造体{
ZEND_ENDIAN_LOHI_3(
zend_uchar タイプ、
zend_uchar フラグ、
uint16_t gc_info)
} v;
uint32_t type_info;
} う;
};
このヘッダーには、refcount (参照カウント)、値の型 type、およびリサイクル関連情報 gc_info と型フラグ flags が格納されます。
次に、各複合型の実装を個別に分析し、PHP5 実装と比較します。参照も複合型ですが、前のパートで紹介したため、ここでは繰り返しません。また、ここではリソース タイプについては説明しません (リソース タイプには何も言うことがないと著者は感じているため)。
文字列

PHP7 は、文字列変数を保存するための新しい構造体 zend_string を定義します。 struct _zend_string {
zend_refcounted gc;
zend_ulong h; /* ハッシュ値 */
size_t レン;
文字値[1];
};
参照カウントされたヘッダーに加えて、文字列にはハッシュ バッファ h、文字列 len の長さ、および文字列 val の値が含まれます。ハッシュキャッシュは、文字列をハッシュテーブルのキーとして使用する場合に、繰り返しハッシュ値を計算する必要がないようにするために存在するため、使用前に初期化してください。
C 言語をよく知らない場合は、val の定義が少し奇妙に感じるかもしれません。この宣言には要素が 1 つしかありませんが、明らかに、格納する文字列は 1 文字より長くなければなりません。ここで実際に使用されているのは、構造体の「ブラック」メソッドです。配列を宣言するときに要素を 1 つだけ定義しますが、実際に zend_string を作成するときに文字列全体を格納するのに十分なメモリを割り当てます。このようにして、val を介して完全な文字列にアクセスできます。
もちろん、読み書きする実際の内容は 1 文字の配列の境界を超えるため、これは型破りな実装方法です。しかし、C コンパイラは、これが行われたことを認識しません。 C99 は「フレキシブル配列」をサポートしているとも明確に述べていますが、私たちの良き友人である Microsoft のおかげで、異なるプラットフォーム上での C99 の一貫性を保証する人は誰もいません (したがって、この方法は、Windows プラットフォームでのフレキシブル配列のサポートの問題を解決することになります)。 )。
新しい文字列型の構造は、ネイティブの C 文字列よりも使いやすくなっています。まず、文字列の長さが直接保存されるため、使用するたびに計算する必要がありません。 2 つ目は、文字列にも参照カウントされたヘッダーがあるため、zval を使用せずに文字列自体をさまざまな場所で共有できることです。頻繁に使用される場所は、ハッシュテーブルのキーを共有することです。
しかし、新しい文字列型には非常に悪い点もあります。zend_string から C 文字列を取り出すのは非常に便利ですが (str->val を使用するだけ)、逆に C 文字列を zend_string に変更する場合は割り当てが必要になります。まず zend_string に必要なメモリを取得してから、その文字列を zend_string にコピーします。これは実際の使用ではあまり便利ではありません。
文字列にはいくつかの固有のフラグもあります (GC フラグに保存されます):
#define IS_STR_PERSISTENT (1/* malloc を使用して割り当て */
#define IS_STR_INTERNED (1/* インターンされた文字列 */
#define IS_STR_PERMANENT (1/* インターンされた文字列がリクエスト境界を存続する */
永続文字列には、単一のリクエストでのみ有効ではなく永続化するために、zend メモリ マネージャー (ZMM) ではなくシステム自体から直接割り当てられたメモリが必要です。この特別な割り当てをマークすると、zval が永続文字列を使用できるようになります。 PHP5 ではこれは行われません。使用前にコピーが ZMM にコピーされます。
予約文字 (インターン文字列) は少し特殊で、リクエストが終了するまで存在し、破棄されるため、参照カウントは必要ありません。予約文字列も重複しないため、新しい予約文字を作成するときは、まず同じ文字がすでに存在するかどうかを確認します。 PHP ソース コード内のすべての不変文字列は予約文字です (文字列定数、変数名、関数名などを含む)。永続文字列は、リクエストが開始される前に作成された予約文字でもあります。ただし、通常の予約文字はリクエストの終了後に破棄されますが、永続的な文字列は常に存在します。
opcache を使用すると、予約文字は共有メモリ (SHM) に保存されるため、すべての PHP プロセス間で予約文字を共有できます。この場合、予約文字は破棄されないため、永続文字列は意味を持ちません。
配列

新しい配列の実装については前の記事で説明したため、ここでは詳しく説明しません。最近のいくつかの変更により、以前の説明の正確性は低下しましたが、基本的な概念は依然として同じです。
ここで話したいのは、前の記事では触れなかった配列関連の概念である不変配列です。これらは本質的に予約文字と似ています。これらは参照カウントを持たず、リクエストが終了するまで存在します (リクエストが終了した後も存在する可能性があります)。
メモリ管理を便利にするため、不変配列は opcache が有効な場合にのみ使用されます。実際の使用例を見てみましょう。まず次のスクリプトを見てください:
for ($i = 0; $i 1000000; ++$i) {
$array[] = ['foo'];
}
var_dump(memory_get_usage());
opcache がオンになっている場合、上記のコードは 32MB のメモリを使用します。これがオンになっていない場合は、$array の各要素が ['foo'] をコピーするため、390MB が必要になります。参照カウント値を増やす代わりに完全コピーを実行する理由は、zend 仮想マシン オペレーター実行時の共有メモリ エラーを防ぐためです。 opcacheを使用しない場合のメモリ爆発の問題は今後改善されることを願っています。
PHP5 のオブジェクト

PHP7 のオブジェクト実装を理解する前に、まず PHP5 を見て、どのような効率上の問題があるかを見てみましょう。 PHP5 の zval は、次のように定義される zend_object_value 構造体を保存します。 typedef struct _zend_object_value {
zend_object_handle ハンドル;
Const zend_object_handlers *ハンドラー;
zend_object_value;
handle はオブジェクトの一意の ID であり、オブジェクト データの検索に使用できます。ハンドルは、オブジェクトのさまざまな属性メソッドを保存する仮想関数テーブル ポインターです。通常、PHP オブジェクトには同じハンドラー テーブルがありますが、PHP 拡張機能によって作成されたオブジェクトは、演算子のオーバーロードによって動作をカスタマイズすることもできます。
オブジェクト ハンドル (ハンドラー) は、「オブジェクト ストレージ」のインデックスとして使用されます。オブジェクト ストレージ自体は、ストレージ コンテナー (バケット) の配列であり、バケットは次のように定義されます。 typedef struct _zend_object_store_bucket {
zend_bool デストラクター_コール済み;
zend_bool は有効です;
zend_uchar apply_count;
ユニオン _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 クローン;
const zend_object_handlers *handlers;
zend_uint refcount;
gc_root_buffer *バッファリング;
} obj;
構造体{
int next;
} free_list;
} バケット;
zend_object_store_bucket;
この構造には多くのものが含まれています。最初の 3 つのメンバーは単なる通常のメタデータです (オブジェクトのデストラクターが呼び出されたかどうか、バケットが使用されたかどうか、オブジェクトが再帰的に呼び出された回数)。次の共用体は、バケットが使用中かアイドル状態かを区別するために使用されます。上記の構造の中で最も重要なのは、struct _store_object サブ構造です:
最初のメンバーである object は、実際のオブジェクト (つまり、オブジェクトが最終的に格納される場所) へのポインターです。オブジェクトは固定長ではないため、実際にはオブジェクト ストレージ バケットに直接埋め込まれません。オブジェクト ポインタの下には、オブジェクトの破棄、解放、複製を管理するために使用される 3 つの操作ハンドル (ハンドラ) があります。ここで注意すべき点は、PHP によるオブジェクトの破棄と解放は別のステップであり、場合によっては前者がスキップされる(不完全な解放)可能性があることです。クローン作成操作は実際にはほとんど使用されません。これは、関係する操作が通常のオブジェクト自体の一部ではないため、(常に) 共有されるのではなく各オブジェクト内で複製されるためです。
これらのオブジェクト ストレージ操作ハンドルの後には、通常のオブジェクト ハンドラ ポインタが続きます。これらのデータは、zval が不明な場合にオブジェクトが破棄される場合があるため、保存されます (通常、これらの操作は zval に対して実行されます)。
バケットには refcount フィールドも含まれていますが、zval 自体にすでに参照カウントが格納されているため、この動作は PHP5 では少し奇妙です。なぜ追加のカウントが必要なのでしょうか?問題は、通常、zval の「コピー」動作は単に参照カウントを増やすことですが、新しい zval を作成して同じ zend_object_value を保存するなど、ディープ コピーが発生する場合があることです。この場合、2 つの異なる zval が同じオブジェクト ストレージ バケットを使用するため、バケット自体も参照カウントする必要があります。この「二重カウント」方法は、PHP5 の実装に固有の問題です。同じ理由で、GC ルート バッファ内のバッファされたポインタも完全にコピーする必要があります。
ここで、オブジェクト ストレージ内のポインターが指す実際のオブジェクトの構造を見てみましょう。通常、ユーザーレベルのオブジェクトは次のように定義されます。 typedef struct _zend_object {
zend_class_entry *ce;
ハッシュテーブル *プロパティ;
zval **properties_table;
ハッシュテーブル *ガード;
zend_object;
zend_class_entry ポインタは、オブジェクトによって実装されたクラス プロトタイプを指します。次の 2 つの要素は、オブジェクト プロパティを保存する異なる方法を使用します。動的プロパティ (クラスで定義されるのではなく実行時に追加されるプロパティ) はすべてプロパティ内に存在しますが、単にプロパティ名と値が一致するだけです。
ただし、宣言されたプロパティには最適化が行われます。各プロパティにはコンパイル中にインデックスが割り当てられ、プロパティ自体は property_table のインデックスに格納されます。プロパティ名とインデックスの一致は、クラス プロトタイプのハッシュテーブルに保存されます。これにより、各オブジェクトがハッシュテーブルの制限を超えるメモリを使用することがなくなり、プロパティのインデックスは実行時に複数の場所にキャッシュされます。
ガードのハッシュ テーブルは、__get などのマジック メソッドの再帰的な動作を実装するために使用されますが、ここでは詳しく説明しません。
上で述べた二重カウントの問題に加えて、この実装にはもう 1 つの問題があります。それは、属性が 1 つだけの最小限のオブジェクトでも 136 バイトのメモリが必要になることです (これには、zval で必要なメモリは含まれません)。そして、中間には多くの間接的なアクセスアクションがあります。たとえば、オブジェクト zval から要素を取り出すには、まずオブジェクトストレージバケットを取り出し、次に zend オブジェクトを取り出す必要があります。その後、オブジェクト属性テーブルと zval を見つけることができます。ポインタを通して。したがって、ここには少なくとも 4 つの間接レベルがあります (実際には、少なくとも 7 つのレベルが必要になる場合があります)。
PHP7 のオブジェクト

PHP7 の実装では、二重参照カウントの削除、メモリ使用量の削減、間接アクセスの削減など、上記の問題の解決が試みられています。新しい zend_object 構造は次のとおりです:
struct _zend_object {
zend_refcounted gc;
uint32_t ハンドル;
zend_class_entry *ce;
Const zend_object_handlers *ハンドラー;
ハッシュテーブル *プロパティ;
zval プロパティテーブル[1];
};
この構造体がオブジェクトのほぼ全体の内容になっていることがわかります。zend_object_value は、オブジェクトとオブジェクト ストレージを直接指すポインターに置き換えられています。完全には削除されていませんが、すでに大きな改善が見られます。
PHP7 の通常の zend_refcounted ヘッダーに加えて、ハンドルとオブジェクト ハンドラーが zend_object に配置されるようになりました。ここのプロパティテーブルも C 構造体のちょっとしたトリックを使用しているため、zend_object とプロパティ テーブルはメモリのブロック全体を取得します。もちろん、属性テーブルはポインターの代わりに zval に直接埋め込まれます。
現在、オブジェクト構造にはガード テーブルがありません。必要に応じて、つまり __get などのメソッドを使用する場合、このフィールドの値は property_table の最初のビットに格納されます。ただし、マジックメソッドを使用しない場合、ガードテーブルは省略されます。
dtor、free_storage、clone の 3 つの操作ハンドルは、以前はオブジェクト操作バケットに保存されていましたが、現在はそれらの構造は次のように定義されています。 struct _zend_object_handlers {
/* 実際のオブジェクトヘッダーのオフセット (通常はゼロ) */
int オフセット;
/* 一般的なオブジェクト関数 */
zend_object_free_obj_t free_obj;
zend_object_dtor_obj_t dtor_obj;
zend_object_clone_obj_t clone_obj;
/* 個々のオブジェクト関数 */
// ...残りは PHP 5 でもほぼ同じです
};
ハンドラー テーブルの最初のメンバーはオフセットですが、これは明らかに操作ハンドルではありません。このオフセットは現在の実装に存在する必要があります。内部オブジェクトは常に標準の zend_object に埋め込まれますが、それにメンバーを追加する必要が常にあるためです。 PHP5 でこの問題を解決する方法は、標準オブジェクトの後にコンテンツを追加することです:
structcustom_object {
zend_object std;
uint32_t 何か;
// ...
};
このようにして、zend_object* を structcustom_object* に簡単に追加できます。これは、C 言語における構造継承の一般的な方法でもあります。ただし、PHP7 でのこの実装には問題があります。zend_object は属性テーブルを保存するときに構造ハック手法を使用するため、zend_object の最後に保存される PHP 属性は、後で追加された内部メンバーを上書きします。したがって、PHP7 の実装では、標準オブジェクト構造の前に独自のメンバーが追加されます。
struct カスタムオブジェクト {
uint32_t 何か;
// ...
zend_object std;
};
ただし、これは、zend_object* と struct custom_object* の間の単純な変換を直接実行できないことも意味します。これは、両方がオフセットによって分離されているためです。したがって、コンパイル時に offsetof() マクロを通じて特定のオフセット値を決定できるように、このオフセットをオブジェクト ハンドラー テーブルの最初の要素に格納する必要があります。
おそらく、zend_object のポインタは直接 (zend_value に) 格納されており、オブジェクト ストレージ内でオブジェクトを探す必要がないのに、なぜ PHP7 のオブジェクト所有者がハンドル フィールドを保持しているのかに興味があるかもしれません。
これは、オブジェクト ストレージが大幅に簡素化されたとはいえ依然として存在するため、ハンドルを保持する必要があるためです。これは単なるオブジェクトへのポインターの配列です。オブジェクトが作成されると、ポインタがオブジェクトのストレージに挿入され、そのインデックスがハンドルに保存されます。オブジェクトが解放されると、インデックスは削除されます。
では、なぜ今オブジェクトストレージが必要なのでしょうか?リクエストの最後に特定のノードがあり、その後ユーザーコードを実行してポインターデータを取得するのは安全ではないためです。この状況を回避するために、PHP は以前のノードですべてのオブジェクトのデストラクターを実行し、そのような操作がなくなるため、アクティブなオブジェクトのリストが必要になります。
また、ハンドルはデバッグにも非常に役立ちます。ハンドルは各オブジェクトに一意の ID を与えるため、2 つのオブジェクトが同じであるか、それとも同じコンテンツを持っているだけかを簡単に区別できます。 HHVM にはオブジェクト ストレージの概念はありませんが、オブジェクト ハンドルも格納されます。
PHP5 と比較すると、現在の実装には参照カウントのみがあり (zval 自体はカウントされません)、メモリ使用量は大幅に削減されています: 基本オブジェクトで 40 バイト、各属性で 16 バイト、そしてこれは zval 後です。間接アクセスの状況も大幅に改善されました。中間層構造が削除されるか直接埋め込まれたため、プロパティを読み取るためのアクセス レベルは 4 つではなく 1 つだけになりました。
間接ズヴァル

これまで、基本的にすべての通常の zval 型について説明してきましたが、特定の状況で使用される特別な型のペアもあります。そのうちの 1 つは、PHP7 で新しく追加された IS_INDIRECT です。
間接 zval は、実際の値が別の場所に格納されることを意味します。 IS_REFERENCE タイプが異なることに注意してください。間接 zval は、zend_reference 構造のように zval を埋め込むのではなく、別の zval を直接指します。
この状況がいつ発生するかを理解するために、PHP での変数の実装を見てみましょう (実際には、オブジェクト プロパティのストレージにも同じことが当てはまります)。
コンパイル中に認識されるすべての変数にはインデックスが割り当てられ、それらの値はコンパイル変数 (CV) テーブル内の対応する場所に保存されます。ただし、PHP では、ローカル変数であってもグローバル変数 ($GLOBALS など) であっても、変数を動的に参照することもできます。この場合、PHP は変数名とその値を含むスクリプトまたは関数のシンボル テーブルを作成します。それらの間のマッピング関係。
しかし問題は、どうすれば 2 つのテーブルへの同時アクセスを実現できるかということです。 CV テーブル内の通常の変数にアクセスできる必要があり、シンボル テーブル内のコンパイルされた変数にアクセスできる必要があります。 PHP5 では、CV テーブルはダブル ポインター zval** を使用します。通常、これらのポインターは中央の zval* テーブルを指し、zval* は最終的に実際の zval:
を指します。 +------ CV_ptr_ptr[0]
| +---- CV_ptr_ptr[1]
| +-- CV_ptr_ptr[2]
|
| +-> CV_ptr[0] --> | +---> CV_ptr[1] --> +-----> CV_ptr[2] --> zval
; シンボル テーブルが必要な場合、zval* を格納する中間テーブルは実際には使用されず、zval** ポインタはハッシュテーブル バケットの対応する場所に更新されます。 3 つの変数 $a、$b、$c があると仮定します。以下は簡単な図です:
CV_ptr_ptr[0] --> SymbolTable["a"].pDataPtr --> zval
CV_ptr_ptr[1] --> SymbolTable["b"].pDataPtr --> zval
CV_ptr_ptr[2] --> SymbolTable["c"].pDataPtr --> いくつかの zval
しかし、PHP7 ではハッシュテーブルのサイズが変更されるとハッシュテーブルバケットが無効になるため、この問題は PHP7 の使用法ではなくなりました。したがって、PHP7 は逆の戦略を使用します。つまり、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 は、IS_UNDEF 型の zval へのポインタにすることもできます。これは、ハッシュテーブルに関連付けられたキーがない場合に発生します。したがって、unset($a) を使用して CV[0] のタイプを UNDEF としてマークすると、シンボル テーブルにキー値 a を持つデータが存在しないと判断されます。
定数とAST

IS_CONSTANT と IS_CONSTANT_AST という 2 つの特別な型について言及する必要があります。これらは PHP5 と PHP7 の両方に存在します。それらを理解するために、まず次の例を見てみましょう:

関数テスト($a = ANSWER,

) $b = 答え * 答え) {
$a + $b を返します;
}

define('ANSWER', 42);
var_dump(test()); // int(42 + 42 * 42) ·
test() 関数の 2 つのパラメータのデフォルト値は定数 ANSWER で構成されますが、関数が宣言された時点では定数の値はまだ定義されていません。定数の正確な値は、define() を介して定義された場合にのみわかります。
上記の問題により、「静的式」を受け入れるパラメータやプロパティ、定数、その他のデフォルト値は、初めて使用されるまで「遅延バインディング」をサポートします。
定数 (またはクラスの静的属性)、つまり「遅延バインディング」を必要とするデータでは、IS_CONSTANT 型 zval が最もよく使用されます。値が式の場合、IS_CONSTANT_AST 型の zval を使用して、式の抽象構文ツリー (AST) を指します。
これで、PHP7 での変数実装の分析は終了です。後で 2 つの記事を書いて、仮想マシンの最適化、新しい命名規則、および一部のコンパイラ インフラストラクチャの最適化を紹介する可能性があります (これは著者のオリジナルの言葉です)。

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