POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

FeedlyRSSTwitterFacebook
Nikita Popov

本記事は、原著者の許諾のもとに翻訳・掲載しております。

前回の投稿では、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には value type それから __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ビットシステム上でのサイズです)。まず、 str obj は両メンバ共に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が含まれています。更に type flags gc_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が個別にヒープに割り当てられなくなったことと、参照カウントを格納しなくなったことです。代わりに、ポインタ先の文字列や配列、オブジェクトなどの複雑な値が参照カウントを格納しています。さらに、割り当てや間接参照が減り、メモリ消費量の減少につながっています。

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

監修者
監修者_古川陽介
古川陽介
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
複合機メーカー、ゲーム会社を経て、2016年に株式会社リクルートテクノロジーズ(現リクルート)入社。 現在はAPソリューショングループのマネジャーとしてアプリ基盤の改善や運用、各種開発支援ツールの開発、またテックリードとしてエンジニアチームの支援や育成までを担う。 2019年より株式会社ニジボックスを兼務し、室長としてエンジニア育成基盤の設計、技術指南も遂行。 Node.js 日本ユーザーグループの代表を務め、Node学園祭などを主宰。