変数の管理
スクリプトで定義される$aのような変数は、整数型、浮動小数点型、文字列型、配列型、オブジェクト型、真偽型、null型、リソース型のすべてが、内部では同じ構造体zvalで管理されています。スクリプトで定義された変数だけではなく、PHPコアで扱われる値に関しても、zvalを使用することが多いです。このzvalがPHP 7になって大幅にチューニングされたことで、全体のパフォーマンス改善に貢献しています。
データ構造がどのように変わったかは重要なので、それを見てみましょう。PHP 5のzvalの定義は以下の通りです。ソースファイルの中では1つの構造体として定義されていませんが、見やすくするためにまとめています。これ以降も、可視性を高めるために、実際の定義方法とは違う書き方をすることがあります。
typedef struct _zval_struct {
union {
long lval;
double dval;
struct {
char *val;
int len;
} str;
HashTable *ht;
struct {
zend_object_handle handle;
zend_object_handlers *handlers;
} obj;
} value;
zend_uint refcount;
zend_uchar type;
zend_uchar is_ref;
} zval;
バイトアライメントの関係で8バイトの倍数となるので、サイズは24バイトになります。PHP 7のzvalは以下のとおりです。
typedef struct _zval_struct {
union {
long lval;
double dval;
zend_refcounted *counted;
zend_string *str;
zend_array *arr;
zend_object *obj;
zend_resource *res;
zend_reference *ref;
void *ptr;
} value;
union {
struct {
zend_uchar type;
zend_uchar flags;
};
zend_uint type_info;
};
zend_uint reserved;
} zval;
zvalのサイズが16バイトに減っています。つまり、PHP 7になるとzvalのサイズがPHP 5の時の2/3になっていますが、24バイトがどのようにして16バイトに減ったのか、PHP 5からPHP 7への変更をみてみましょう。
整数型や浮動小数点型、配列型やオブジェクト型を管理する共用体valueが構造体を持たなくなったことにより、8バイト減っています。参照やコピーオンライトを管理するrefcountとis_refがなくなったので、6バイト減っています。変数の種類を管理するtypeは、管理方法が少し異なり、2バイト増えています。新しく加わったメンバはreservedで、この4バイトも増えています。これらを計算すると、以下のように24バイトから16バイトへとなります。
24 - 8 - 6 + 2 + 4 = 16
PHP 7では、整数型、浮動小数点型、真偽型などがコピーオンライトではなくなったので、refconutとis_refをzvalで持つことをやめました。コピーオンライトになっており、参照を管理している構造体zend_refcountedを持っているのは、文字列型、配列型、オブジェクト型、リソース型、参照型だけです。PHP 5では最大公約数的にどの型も等しく持っていたメンバを、PHP 7ではそれぞれの変数の種類によって最適化したといってよいでしょう。
いくつか代表的な型の構造体をみてみます。文字列型の構造体は以下です。
typedef struct _zend_string {
zend_refcounted gc;
zend_ulong h;
size_t len;
char val[1];
} zend_string;
gcは参照を管理しているメンバで、hには文字列のハッシュ値が入っています。lenは文字列の長さで、valは文字列のサイズによって大きくなります。ハッシュ値を持っているので、探索する時にパフォーマンスが高くなるでしょう。
次は配列型の構造体です。PHP 7から使われている構造体で、PHP 5では、zvalの中にHashTableがあったので必要ありませんでした。
typedef struct _zend_array {
zend_refcounted gc;
HashTable ht;
} zend_array;
文字列型の構造体と同じように、参照を管理するgcがあります。htが配列のキーと値を管理しているハッシュテーブル構造体です。PHP 5ではハッシュテーブル構造体がzvalのメンバでしたが、PHP 7からはzvalからarrayポインタを1つたどるのでアクセスが悪くなったと言えます。ハッシュテーブルについては、のちほど詳しく説明します。
次はオブジェクト構造体です。まずはPHP 5から紹介します。
typedef struct _zend_object {
zend_class_entry *ce;
HashTable *properties;
HashTable *guards;
} zend_object;
PHP 7のオブジェクト構造体は以下です。
typedef struct _zend_object {
zend_refcounted gc;
uint32_t handle;
zend_class_entry *ce;
const zend_object_handlers *handlers;
HashTable *properties;
HashTable *guards;
zval properties_table[1];
} zend_object;
比べてみると、PHP 7になるとメンバが増えています。これは、PHP 5ではあった構造体zend_object_valueがなくなったからで、オブジェクト識別子handleがzend_objectに移動しました。PHP 7からはproperties_tableが新しく増えており、オブジェクトを生成する時に、プロパティの数だけ動的に配列が確保されます。プロパティの値はプロパティと共にハッシュテーブルのpropertiesに入っています。プロパティだけが管理されたproperties_tableは連続した配列なので、プロパティを探索する際には早くなります。
PHP 7でもコピーオンライトを用いる場合に使用する構造体をみてみましょう。
struct _zend_refcounted {
uint32_t refcount;
union {
struct {
zend_uchar type;
zend_uchar flags;
uint16_t gc_info;
} v;
uint32_t type_info;
} u;
};
参照カウントrefcountはこの構造体で持っています。typeとflagsはzvalでも持っていますが、こちらでも持っているので、もとのzvalをたどるというオーバーヘッドがなくなっています。
次は具体的な例で、変数の扱いがどのように変わったのかを比べてみましょう。まずは整数型から。
$a = 1;
上記のスクリプトでは、PHP 5とPHP 7のメモリイメージは以下となります。
比べてみると、PHP 7の方がサイズが小さいことが改めて分かります。次に別の変数に代入してみます。
$b = $a;
上記のスクリプトでは、PHP 5とPHP 7のメモリイメージは以下となります。
PHP 5ではzvalが1つしかありませんが、PHP 7ではzvalが2つになっていることが分かるでしょう。PHP 7では整数型に対してコピーオンライトではなくなったので、実体を持っています。使用するメモリを比べてみると、PHP 5では40バイトあったものが、PHP 7では32バイトとなり、少なくなっています。
$c = $a;
さらに値を入れていくとこうなります。
PHP 5では引き続きzvalが1つしかありませんが、PHP 7ではzvalが3つになっています。使用するメモリを比べてみると、PHP 5では48バイトあり、PHP 7では48バイトとなり、同じになりました。このように、変数の代入が増えていくにつれて、PHP 5の方が使用するメモリは少なくなっていきますが、PHP 5では値を参照するためにはポインタをたどっていく必要があり、これがオーバーヘッドとなっていました。
次に文字列型をみてみましょう。
$a = 'abcdefgh';
上記のスクリプトでは、PHP 5とPHP 7のメモリイメージは以下となります。
PHP 5では40バイトを使用しており、PHP 7では48バイトを使用しています。PHP 7の方が多くメモリを使用していますが、文字列の実体へはPHP 5ではポインタを3回たどるのに対して、PHP 7ではポインタを2回しかたどりません。これは文字列の実体がzval_string内にあるためです。ポインタをたどる回数が少ないことで、メモリアクセスの効率がよくなっています。
