PHP7における内部値の表現―パート1 : PHP5とPHP7のzvalの仕組み

前回の投稿では、PHP7に取り入れられたimprovements to the hashtable implementation(ハッシュテーブル実装の改善)について書きました。それに続く形で、今回はPHPの全般的な値の新しい表現形式に目を向けてみます。

取り上げる内容の量から考えて、2つのパートに分ける予定です。このパートでは、PHP5とPHP7の間におけるzval(Zend値)の実装の違い、それから参照の実装について説明し、パート2では、文字列やオブジェクトなど、個別の型の実現について、詳細に検討していこうと思います。

PHP5のzval

PHP5では、zval構造体は次のように定義されます。

typedef struct _zval_struct {
    zvalue_value value;
    zend_uint refcount__gc;
    zend_uchar type;
    zend_uchar is_ref__gc;
} zval;

見ての通り、zvalにはvaluetypeそれから__gc情報(後ほど話します)が含まれています。valueメンバは、zvalに格納できる複数の値の共用体です。

typedef union _zvalue_value {
    long lval;                 // For booleans, integers and resources
    double dval;               // For floating point numbers
    struct {                   // For strings
        char *val;
        int len;
    } str;
    HashTable *ht;             // For arrays
    zend_object_value obj;     // For objects
    zend_ast *ast;             // For constant expressions
} zvalue_value;

C言語の共用体は、1度に1つのメンバのみがアクティブになる構造で、そのサイズは、最も大きなメンバのサイズと同じです。共用体の全てのメンバは、同じメモリ領域に保持され、アクセスされるメンバに応じて異なる解釈がされます。例えば上記の共用体のlvalメンバを読み込めば、その値は符号付き整数として解釈され、dvalメンバを読み込めば、値は代わりに倍精度浮動小数点数として解釈されるというわけです。

共用体のどのメンバが使用中であるかを把握するため、zvalのtypeプロパティは整数の型タグを保持します。

#define IS_NULL     0      /* Doesn't use value */
#define IS_LONG     1      /* Uses lval */
#define IS_DOUBLE   2      /* Uses dval */
#define IS_BOOL     3      /* Uses lval with values 0 and 1 */
#define IS_ARRAY    4      /* Uses ht */
#define IS_OBJECT   5      /* Uses obj */
#define IS_STRING   6      /* Uses str */
#define IS_RESOURCE 7      /* Uses lval, which is the resource ID */

/* Special types used for late-binding of constants */
#define IS_CONSTANT 8
#define IS_CONSTANT_AST 9

PHP5の参照カウント

PHP5のzvalは(少数の例外を除いて)ヒープに割り当てられており、PHPは、どのzvalが使用中で、どのzvalを解放すべきかを把握しなければなりません。この目的のために参照カウントが取り入れられています。ある時点におけるzvalの”参照されている”頻度を、zval構造体のrefcount__gcメンバが保持します。例えば$a = $b = 42では、値42が2つの変数から参照されているため、refcountは2です。もしrefcountが0なら、その値は使われていないということなので、解放することができます。

ちなみに、refcountが参照するリファレンス(値が現在、使用されている回数)はPHPの(を使う)リファレンスとは何の関係もないのでご注意ください。以降は2つのあいまいさを回避するため、用語を分けて”リファレンス”と”PHPリファレンス”としますが、今のところ、PHPリファレンスのことは無視しておきましょう。

参照カウントと密に関係しているコンセプトと言えば”コピーオンライト”です。zvalを複数ユーザで共有している時、その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)

// The following line causes a zval separation
$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); // zval_1 is destroyed, because refcount=0
           // $a -> zval_2(type=IS_LONG, value=43, refcount=1)

参照カウントには重大な欠陥が1つあります。循環参照を検出して解除できないということです。これに対応するため、PHPは追加のサイクルコレクタを使っています。zvalのrefcountがデクリメントされ、循環の一部になりそうな時は、zvalが”ルートバッファ”に書き込まれます。このルートバッファがいっぱいになると、マーク・アンド・スイープ・ガベージコレクションにより、可能性のある循環が収集される仕組みです。

追加のサイクルコレクタをサポートするため、実際のzval構造体は以下のようになります。

typedef struct _zval_gc_info {
    zval z;
    union {
        gc_root_buffer       *buffered;
        struct _zval_gc_info *next;
    } u;
} zval_gc_info;

zval_gc_infoの構造体には、通常のzvalと共に追加のポインタが埋め込まれています。なお、uは共用体(union)を表しているので、実際のところ、これは1つのポインタで2つの型を指しているということに注意してください。bufferedポインタは、zvalがルートバッファ内のどこで参照されているかを保持するために使われており、サイクルコレクタが実行する前に破壊された場合は(その可能性は非常に高いです)削除しても構いません。ちなみにコレクタが値を破壊する時にはnextが使われますが、それについてはここでは割愛します。

変更の要因

ここでサイズについて話をしたいと思います(以下、全てのサイズは64ビットシステム上でのサイズです)。まず、strobjは両メンバ共に16バイトのため、zvalue_valueの共用体も16バイトになります。zval全体の構造体は(パディングにより)24バイトで、zval_gc_infoは32バイトです。それと、ヒープにzvalを割り当てることで、アロケーションオーバーヘッドの16バイトが加算されます。zvalは複数の場所で使うことができますが、それはともかくとして、zvalごとに48バイト使う計算になりますね。

ここで、このzval実装の(数ある)非効率性について考えてみましょう。zvalが整数を保持する単純なケースを頭に思い描いてみてください。その場合、それ自体では8バイトになります。加えて、どんな場合でも型タグの格納が必要となってきます。これについては、本来なら1バイトですが、パディングのせいで8バイトが必要とされます。

(最初の概算で)実際に”必要”とされるこの16バイトに加え、参照カウントと循環収集のための16バイトが必要となり、さらにアロケーションオーバーヘッドの16バイトも必要になります。言うまでもありませんが、割り当てと解放は実際にやらなければならず、どちらとも非常に高コストな演算です。

ここで、ある疑問が生じます。単純な整数値は、参照カウントされる値や循環収集される値として、またヒープに割り当てられる値として、本当に保持する必要があるのでしょうか? その答えはもちろんノーです。これは理にかなっていませんよね。

以下にPHP5におけるzval実装の主な問題点をまとめてみました。

  • (ほとんど)常にzvalのヒープ割り当てが必要となる。
  • zvalは、値に共有する価値がなく(整数)、循環が形成できないような場合でも、常に参照カウントされ、循環収集の情報を有している。
  • オブジェクトとリソースの場合、zvalを直接、参照カウントすると、参照カウントが重複する。この理由については、次のパートで説明します。
  • 場合によっては非常に多くの間接参照を要する。例えば変数に格納されたオブジェクトにアクセスするには、合計4つのポインタを逆参照しなければならない(つまり長さ4のポインタチェーンをたどる必要がある)。なお、これについても次のパートで話したいと思っています。
  • zvalを直接、参照カウントするということは、zval間でしか値が共有できないことを意味している。例えば、zvalとハッシュテーブルキーの間だと、(ハッシュテーブルのキーをzvalとして格納しない限り)文字列は共有できない。

PHP7のzval

ではPHP7で新しくなったzvalの実装についてお話しします。基本的な変更点として、zvalはもはや個別にヒープに割り当てられることはなく、自身にrefcountを保持するものでもなくなりました。代わりに、zvalがポイントする先の(文字列、配列、オブジェクトなどの)複雑な値自身にrefcountが格納されます。これにより次のような利点があります。

  • 単純な値の場合、割り当ての必要はなく、参照カウントも使用しない。
  • 参照カウントが重複することはない。オブジェクトの場合、そのオブジェクトのrefcountのみが使用される。
  • refcountが値自体に格納されるようになったため、zval構造体とは関係なく個別に値を共有できる。文字列はzvalとハッシュテーブルキーの両方で使用できる。
  • 間接参照が減る。値に到達するまでに経由するポインタの数が少なくなる。

ではzvalの定義方法を見てみましょう。

struct _zval_struct {
    zend_value value;
    union {
        struct {
            ZEND_ENDIAN_LOHI_4(
                zend_uchar type,
                zend_uchar type_flags,
                zend_uchar const_flags,
                zend_uchar reserved)
        } v;
        uint32_t type_info;
    } u1;
    union {
        uint32_t var_flags;
        uint32_t next;                 // hash collision chain
        uint32_t cache_slot;           // literal cache slot
        uint32_t lineno;               // line number (for ast nodes)
        uint32_t num_args;             // arguments number for EX(This)
        uint32_t fe_pos;               // foreach position
        uint32_t fe_iter_idx;          // foreach iterator index
    } u2;
};

最初のメンバは前と同様、valueの共用体です。次のメンバは型情報を格納する整数で、共用体を使って更に分割されています(ZEND_ENDIAN_LOHI_4は、異なるエンディアンのプラットフォーム間でレイアウトを引き継ぐためのマクロですので無視してください)。このサブストラクチャで重要なのは、type(以前のバージョンと同様です)とtype_flagsです。それについては今から説明します。

この時点でちょっとした問題があります。それは、valueメンバは8バイトの大きさですが、パディングが挿入されるため、1バイト追加するだけでzvalのサイズは16バイトにふくれてしまうのです。当然、型を格納するためだけに、8バイトも必要ありません。そのためzvalはもう1つの共用体u2を持っています。これはデフォルトでは使用しませんが、周辺のコードから、4バイトのデータを格納する目的を持たせることができます。さまざまな共用体メンバが、この予備のデータスロットをそれぞれの用途で使用します。

PHP7では、value共用体はわずかに変更されます。

typedef union _zend_value {
    zend_long         lval;
    double            dval;
    zend_refcounted  *counted;
    zend_string      *str;
    zend_array       *arr;
    zend_object      *obj;
    zend_resource    *res;
    zend_reference   *ref;
    zend_ast_ref     *ast;

    // Ignore these for now, they are special
    zval             *zv;
    void             *ptr;
    zend_class_entry *ce;
    zend_function    *func;
    struct {
        ZEND_ENDIAN_LOHI(
            uint32_t w1,
            uint32_t w2)
    } ww;
} zend_value;

まず、value共用体は16バイトではなく8バイトになっていることに注目してください。整数(lval)と倍精度浮動小数点数(dval)のみ直接格納することができます、他はいずれもポインタです。全てのポインタ型は(上記の例でspecial(特別)とされているものを除き)、参照カウントを使用し、zend_refcontedで定義された共通のヘッダを持っています。

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が含まれています。更にtypeflagsgc_infoもあります。typeが行うのは、zval型を複製し、zvalを格納せずに参照カウントされた別の構造体をGCに識別させることです。flagsは異なる型に異なる目的で使用されますが、それについては次のパートで個別に説明します。

gc_infoは、従来のzvalのbufferedエントリと同じですが、ポインタをルートバッファに格納する代わりに、インデックスを含むようになりました。ルートバッファは固定サイズ(10000エレメント)で、64ビットのポインタの代わりに16ビット数を使用するには十分です。gc_info情報は、コレクションを行っているノードの識別に使用する”色”を符号化します。

zvalのメモリ管理

zvalが個別にヒープに割り当てられないことは既に言及しました。しかし、当然どこかの領域には配置される必要があります。ではどのように機能するのでしょうか? zvalはヒープに割り当てられた構造の一部でありながら、直接埋め込まれているのです。例えば、ハッシュテーブルのバケツが、別のzvalを指すポインタを格納する代わりに直接zvalを埋め込むのです。関数のコンパイル済みの変数テーブルやオブジェクトのプロパティテーブルはzval配列になり、1つのチャンクとしてアロケーションされます。別のzvalへのポインタを格納することはありません。したがって新しいzvalは間接参照が1段階少なくなります。以前zval*だったものが今ではzvalになるのです。

新しい領域でzvalが使用される際、以前はzval*をコピーして、そのrefcountをインクリメントする必要がありました。新しいバージョンでは、zvalの中身(u2は無視します)をコピーし、例えばその値が参照カウントを使用する場合は、それがポイントしているrefcountの値を1つ増やすかもしれないということになります。

値が参照カウントされていることを、PHPはどのように把握するのでしょうか? 単に型のみでは判断できません。文字列や配列などの型が必ず参照カウントされるとは限らないからです。代わりに1ビットのzval type_infoメンバが、そのzvalが参照カウントされているかどうかを決定します。型のプロパティを符号化しているビットは他にもたくさんあります。

#define IS_TYPE_CONSTANT            (1<<0)   /* special */
#define IS_TYPE_IMMUTABLE           (1<<1)   /* special */
#define IS_TYPE_REFCOUNTED          (1<<2)
#define IS_TYPE_COLLECTABLE         (1<<3)
#define IS_TYPE_COPYABLE            (1<<4)
#define IS_TYPE_SYMBOLTABLE         (1<<5)   /* special */

型が持つことの出来る最初の3つのプロパティは”refcounted(参照カウントされる)
“、”collectable(コレクションできる)”、”copyable(コピーできる)”です。refcountedについてはもうご存知ですね。collectableはそのzvalがサイクルに参加できるかどうかを表します。例えば、文字列は(頻繁に)参照カウントされますが、そこの文字列でサイクルを作ることはできません。

copyableは”複製”が実行されたときに、その値がコピーされる必要があるかどうかを決定します。複製とはハードコピーを意味します。例えば、ある配列をポイントしているzvalを複製する場合、単純にその配列のrefcountを増やすことにはならず、新しく別の配列が生成されます。しかし、オブジェクト型やリソース型など場合は、複製であってもrefcountをインクリメントすることになります。このような型はnon-copyable(コピーできない)と呼ばれます。これはオブジェクトやリソースを渡すセマンティクスと合致します(念のため言っておくと、これらはリファレンスで渡されるのではありません)。

次のテーブルはそれぞれの型と、その型が使用するフラグを示したものです。”Simple types”は、別の構造体へのポインタを使用しない整数値やブーリアン値などを指します。”immutable”フラグのカラムもあります。これは、不変の配列を識別するために使用されますが、詳細は次のパートで説明します。

                | refcounted | collectable | copyable | immutable
----------------+------------+-------------+----------+----------
simple types    |            |             |          |
string          |      x     |             |     x    |
interned string |            |             |          |
array           |      x     |      x      |     x    |
immutable array |            |             |          |     x
object          |      x     |      x      |          |
resource        |      x     |             |          |
reference       |      x     |             |          |

ここで、zval管理が実際どのように行われているか、例を2つ見てみましょう。まずは上のPHP5の例から整数を使用する例です。

$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がアロケーションされているのではなく埋め込まれているということを忘れないでください。変数を設定しない場合、当該の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を使用していますが、どのzvalも同じ(参照カウントされた)zend_array構造体をポイントしています。変更されると、その配列は複製されなければいけません。この例はPHP5での動きと似ています。

PHP7でサポートされている型を詳しく見てみましょう。

// regular data types
#define IS_UNDEF    0
#define IS_NULL 1
#define IS_FALSE    2
#define IS_TRUE 3
#define IS_LONG 4
#define IS_DOUBLE   5
#define IS_STRING   6
#define IS_ARRAY    7
#define IS_OBJECT   8
#define IS_RESOURCE 9
#define IS_REFERENCE    10

// constant expressions
#define IS_CONSTANT 11
#define IS_CONSTANT_AST 12

// internal types
#define IS_INDIRECT 15
#define IS_PTR  17

上記の項目はPHP5でサポートされているものとほとんど変わりませんが、追加されたものがいくつかあります。

  • IS_UNDEF型は、NULL zval(IS_NULL zvalではありません)ポインタが使われていた場所で使われる。上記の参照カウントの例では、unsetされた変数に対してIS_UNDEF型が設定される。
  • IS_BOOL型は、IS_FALSE型とIS_TRUE型に分けられている。そのため、ブーリアン値は型に符号化され、複数の型チェックを最適化することができる。しかし、”ブーリアン”型であることには変わらず、ユーザーに分かりやすい変更である。
  • PHPリファレンスではzvalのis_refフラグが廃止され、代わりにIS_REFERENCE型が使われている。これがどのように機能するかについては次のセクションで説明します。
  • IS_INDIRECT型とIS_PTR型は特別な内部的型になる。

IS_LONG型は普通のC言語longの代わりに、zend_longが値として使われています。今までは、64ビットWindows(LLP64)でも、long型は32ビット幅しかなかったので、PHP5でも32ビット整数を使うしかありませんでした。しかし、PHP7では、使っているOSが64ビットであれば、Windowsでも、64ビット整数が使えるようになっています。

次のパートで、zend_refcount型を詳しく説明します。ここでは、実装されているPHPリファレンスを見ていきたいと思います。

リファレンス

PHP7におけるPHP&リファレンスの扱いは、PHP5でのアプローチとは全く異なります。これが、PHP7でバグが発生する主な原因であると言えると思います。まずは、PHP5ではリファレンスがどのように機能しているのか見ていきましょう。

通常のコピーオンライトの原理では、zvalを書き換える前に、zvalを共有している全ての場所の値が変更されないよう、原本とコピーを分ける必要があるとされています。書き換え時にコピーすることで、値渡しのセマンティクスに合わせています。

しかし、この原理はPHPリファレンスには当てはまりません。値がPHPリファレンスである場合、その値を使う全ての場所に変更を適用させる必要があります。PHP5では、zvalの一部であったis_refフラグによって、値がPHPリファレンスであるか、書き換える前に分ける必要があるかを判断します。例えば下記のようになります。

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

$b[] = 1;   
    // $a = $b = zval_1(type=IS_ARRAY, refcount=2, is_ref=1) -> HashTable_1(value=[1])
    // is_ref=1なので、PHPはzvalを複製/分離しない

このデザインの重大な問題は、PHPリファレンスである変数とそうでない変数で値を共有できないことです。下記の例をご覧ください。

$a = [];    
    // $a   -> zval_1(type=IS_ARRAY, refcount=1, is_ref=0) -> HashTable_1(value=[])
$b = $a;    
    // $a, $b   -> zval_1(type=IS_ARRAY, refcount=2, is_ref=0) -> HashTable_1(value=[])
$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) -> HashTable_1(value=[])
    // $c, $d   -> zval_1(type=IS_ARRAY, refcount=2, is_ref=1) -> HashTable_2(value=[])
    // $dは$cへのリファレンスであって、$aや$bへのリファレンスではないため、ここでzvalがコピーされる
    // 必要がある。そのため、同一のzvalについて、is_ref=0のものとis_ref=1のものが一旦同居する。

$d[] = 1;
    // $a, $b   -> zval_1(type=IS_ARRAY, refcount=2, is_ref=0) -> HashTable_1(value=[])
    // $c, $d   -> zval_1(type=IS_ARRAY, refcount=2, is_ref=1) -> HashTable_2(value=[1])
    // 分離されたzvalなので、$d[] = 1は $a と $b を変更しない。

PHPでは、最終的に普通の値よりリファレンスの値を使う方が動きを遅くしてしまいますが、このリファレンスの動きがその原因の1つです。これが問題となる場合の簡単な例は下記の通りです。

$array = range(0, 1000000);
$ref =& $array;
var_dump(count($array)); // <-- ここで分離が起こる

count()は値を値渡しとして受け取りますが、$arrayはPHPリファレンスのため、配列のコピーを全て完了してからcount()へ渡されます。$arrayがリファレンスでなければ、count()$arrayは同じ値を共有できます。

では、PHP7に実装されたリファレンスを見ていきましょう。PHP7ではzvalが個別に割り当てられなくなっているため、PHP5と同じアプローチはできません。その代わり、zend_reference構造を値とするIS_REFERENCE型が追加されています。

struct _zend_reference {
    zend_refcounted   gc;
    zval              val;
};

つまり、zend_referenceは単に参照カウントされたzvalなのです。リファレンス集合内の変数は、IS_REFERENCE型のzvalとして同じzend_referenceのインスタンスに関数ポインタが指しています。val zvalは他のzvalと同じように振る舞う上に、ポインタ先が複雑な値であっても共有することが可能です。例えば、リファレンスである変数とそうでない変数の間で配列を共有することができます。

もう一度上記のコードを見てみましょう。今度はPHP7での動きを見ます。簡潔にするために、各変数のzvalを書かずにポインタ先の構造のみを書きます。

$a = [];
    // $a       -> zend_array_1(refcount=1, value=[])
$b =& $a;
    // $a, $b -> zend_reference_1(refcount=2)   -> zend_array_1(refcount=1, value=[])

$b[] = 1;
    // $a, $b -> zend_reference_1(refcount=2)   -> zend_array_1(refcount=1, value=[1])

参照渡しでの代入で、新しいzend_referenceが作成されました。ちなみに、リファレンスの値は(変数がPHPリファレンス集合の一部のため)refcountが2ですが、値自体は(1つのzend_reference構造のポインタ先であるため)refcountが1です。では、リファレンスである変数とそうでない変数が混在する場合を考えてみましょう。

$a = [];    // $a   -> zend_array_1(refcount=1, value=[])
$b = $a;    // $a, $b,  -> zend_array_1(refcount=2, value=[])
$c = $b // $a, $b, $c   -> zend_array_1(refcount=3, value=[])

$d =& $c;   // $a, $b   -> zend_array_1(refcount=3, value=[])
    // $c, $d   -> zend_reference_1(refcount=2) ---^
    // PHPリファレンスであるかないかに関わらず、
    // 全ての変数が同一のzend_arrayを共有している

$d[] = 1;   // $a, $b       -> zend_array_1(refcount=2, value=[])
    // $c, $d -> zend_reference_1(refcount=2)   -> zend_array_2(refcount=1, value=[1])
    // この時点でのみ、代入が起こってzend_arrayが複製される

PHP5からの重要な変更点は、リファレンスである変数とそうでない変数の全てを同じ配列で共有することが可能であることです。ただし、書き換えを行うと配列は分かれます。つまり、PHP7で、安心して大きな参照配列をcount()に渡すことができ、重複することがありません。リファレンスの値が普通の値より動きを遅くしてしまうのは、zend_referenceの割り当て(および間接参照)をする必要があり、必ずしもそれが速いエンジンコードのパスで行われていないからです。

まとめ

要約すると、PHP7での主な変更点は、zvalが個別にヒープに割り当てられなくなったことと、参照カウントを格納しなくなったことです。代わりに、ポインタ先の文字列や配列、オブジェクトなどの複雑な値が参照カウントを格納しています。さらに、割り当てや間接参照が減り、メモリ消費量の減少につながっています。

次のパートでは複雑な型についてご説明します。