PHP7变量的内部实现(一)

PHP7变量的内部实现-part 1

本文翻译自Nikita的文章,水平有限,若有错误,欢迎指正查看原文php

受篇幅限制,这篇文章将分为两个部分。本部分会讲解PHP5和PHP7在zval结构体的差别,同时也会讨论引用的实现。第二部分会深刻探究一些数据类型如string和对象的实现。html

PHP5中的zval

PHP5中zval结构体的定义以下:node

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

能够看到,zval由value、type和一些额外的__gc信息组成。__gc与垃圾回收相关,咱们稍后讨论。value是一个共用体,能够存储y一个zval各类可能的值。git

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语言中,共用体的尺寸与它最大的成员尺寸相同,在某一时刻只能有一个成员处于活动状态。共用体全部的成员都存储在相同的内存,根据你访问的成员不一样,内容会被解释成不一样的类型。以上面的共用体为例,若是访问lval,值将被解释为一个有符号整型;而访问dval将被解释成双精度浮点型。以此类推。github

为了弄清结构体中哪一个成员处于活动状态,zval会存储一个整型type来标识具体的数据类型。express

#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在被使用,哪些应该被释放。为达到这个目的,引用计数被使用。引用计数即在结构体中用refcount__gc成员来记录该结构体被“引用”了多少次。例如,在$a = $b = 42中,42被两个变量引用,因此它的引用计数为2。若是引用计数变成0,则意味着该值没被使用,能够被释放。windows

须要注意的是引用计数的“引用”(即一个值被引用的次数)与“PHP引用”($a=&$b)毫无关系。在接下来的内容里,我会始终使用“引用”和“PHP引用”这两个术语来释疑这两个概念。就当前来讲,咱们先把“PHP引用”放在一边。数组

与引用计数密切相关的一个概念是“写时复制”(copy on write):zval只能在其内容未被修改的时候才能在多个变量间共享。要实现修改,zval必选被复制(分离),而改动只能在复制出的zval上进行。安全

如下例子展现了写时复制和zval销毁。php7

$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); // zval_1 被销毁,由于其refcount=0
           // $a -> zval_2(type=IS_LONG, value=43, refcount=1)

引用计数有一个致命缺陷:它不能检测和释放循环引用。为解决这个问题,PHP额外使用了环收集器。当一个zval的引用计数减小的时候,它就有必定概率是循环引用的一部分,该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是一个共用体,也就是说实际上只有一个指针,它可能指向两种不一样的类型。buffered指针用来存储zval在根缓冲区中的引用位置,若是zval在环收集器运行以前就被销毁(这是很是可能的),那么该指针将会从根缓冲区移除。next指针在收集器销毁值的时候会被用到,可是我不会深刻讲解这一点。

改进的动机

先讨论一下基于64位系统的内存占用。首先,zvalue_value共用体占用16个字节,由于它的str和obj成员都那么大。整个zval结构体一共24个字节(因为内存对齐[padding]),而zval_gc_info是32字节。除此以外,在堆分配的过程当中,又增长了16字节的分配开销。由此一个zval就占用48字节--尽管该zval可能在多个地方都被用到。

如今咱们就能够分析下这种zval实现方式低效的地方。考虑用zval存储整数的状况,整数占用8个字节,另外类型标示是必需的,它自己占用一个字节,可是因为内存对齐,实际上就要加上8个字节。

这16字节是咱们真正“须要”的空间(近似的),此外,为了处理引用计数和垃圾回收,咱们增长了16字节;因为分配开销又增长了另外16字节。更不用提还要处理分配和后续的释放,这都是很昂贵的操做。

由此引起了一个问题:一个简单的整数真的须要存储为一个有引用计数、可垃圾回收,而且是堆分配的值吗?答案固然是不须要,这样作是没道理的。

如下概述了PHP5中zval实现方式的一些主要问题:

  • zval(几乎)老是须要堆分配。
  • zval老是会被引用计数且携带环收集信息,即便是在共享值不划算(好比整数)和不能造成引用环的状况下。
  • 当处理对象和资源时,直接对zval进行引用计数会致使双重计数。缘由会在下一部分讨论。
  • 某些状况会引入不少的间接操做。好比为了访问一个对象,一共要进行4次指针跳转。这也将在下一篇中分析。
  • 直接对zval进行引用计数意味着值只能在zval间共享。好比咱们不能在zval和哈希表key之间共享一个字符串(不将哈希表key用zval变量存放)。

PHP7中的zval

经过以上讨论,咱们引进了PHP7新的zval实现。最根本的改变是zval再也不是堆分配且它自身再也不存储引用计数。相反的,对zval指向的任何复杂类型值(如字符串、数组、对象),这些值将本身存储引用计数。这有如下优势:

  • 简单值不须要分配且不用引用计数。
  • 再也不有双重引用计数。对对象来讲,只有在对象自己存在引用计数。
  • 因为引用计数保存在值中,这个能够独立于zval结构而被复用。同一个字符串能同时被zval和哈希表key引用。
  • 间接操做少了不少,也就是说在获取一个值的时候须要跳转的指针数量变少了。

新的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字节空间,因为结构体内存对齐,即便增长一个字节也会让zval内存增加到16字节。然而很明显咱们不须要8个字节来仅仅存放类型信息。这就是为何此zval包含了一个额外的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;

首先要注意到这个共用体占用8字节而不是16字节。它仅仅会直接存储整数(lval)和双精度浮点数(dval),对其它类型它都会存储对应指针。全部的指针类型(除了什么代码中标记为特殊的)都会引用计数而且有一个通用的头部,定义为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;
};

不用说这个结构会包含引用计数。另外,它还包含type、flags和gc_info。type是复制的zval的type,它使得GC在不存储zval的状况下就能区分不一样的引用计数结构。根据类型的不一样,flags有不一样的使用目的,这些会在下一部分按类型分别讨论。

gc_info等同于老zval中的buffered成员。不一样的是它存储了在根缓冲区中的索引,来代替以前的指针。由于跟缓冲区尺寸固定(10000个元素),用16字节的数子而不是64位的指针就足够了。gc_info还含有该节点的“颜色”信息,这在垃圾回收中用来标记节点。

zval内存管理

我已经提到zval再也不是单独的堆分配。然而很明显它仍然须要被存在某个地方,那么这是怎么实现的呢?尽管zval大多数时候还是堆分配数据结构的一部分,不过它们是直接嵌入到这些数据结构中的。好比哈希表就会直接内置zval而不是存放一个指向另外一zval的指针。函数的编译变量表或者对象的属性表会直接保存为一个拥有连续内存的zval数组,而再也不存储指向散落各处zval的指针。所以当前的zval存储一般都会少了一层的间接引用,也就是说如今的zval至关于以前的zval*。

当一个zval在新的地方被引用时,按照以前的方式,就意味着要复制zavl*并增长它的引用计数。如今则须要复制zval的内容,同时若是该zval指向的值用到引用计数的话则还要增长该值的引用计数。

PHP是如何知道一个值是否用到引用计数的呢?这不能仅仅依靠类型来判断,由于有些类型好比字符串和数组并不老是引用计数的。相反的,会根据构成zval的type_info的一个字节来判断是否引用计数。另外还有其它几个字节编码了该类型的一些特征。

#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 */

一个类型能拥有的三个主要特征是引用计数、可回收和可复制。引用计数的含义已讨论过,可回收意味着该zval可能参与循环引用。举例来讲,字符串(一般)是引用计数的,可是却无法用字符串构造一个引用环。

可复制性决定了在为一个变量建立“副本”的时候它的值是否须要执行拷贝。副本是硬拷贝,好比复制指向数组的zval时,就不是简单的增长数组的引用计数,而是要建立该数组的一个新的独立拷贝。然而对对象和资源这些类型来讲,复制应该仅仅增长引用计数--这些类型就是所谓的不可复制。这与对象和资源在进行传递时的语义相符(当前不是引用传递)。

如下表格展现了不一样类型和它们所用的标识。“简单类型”指整数和布尔值这类不须要用指针指向一个单独结构的类型。同时还用一列展现了“不可变”标记,它用来标记不可变数组,这将在下一部分详细讨论。

| 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管理是如何工做的。先基于上文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)

这个例子挺无趣的。简单来讲就是整型不会再被共用,这些变量都有单独的zval。不要忘了zval再也不须要单独分配,它们是内嵌的,我经过把->换成=来表示这种变化。unset一个变量会把对应zval的type设置为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) and zend_array_2 is destroyed
           // $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时,zval的类型就被置为IS_UNDEF。
  • IS_BOOL类型被细分红了IS_FALSE和IS_TRUE。由此布尔变量的值就被编码在类型中,这就使得一些基于类型检查的优化成为可能。这个改变对用户层是透明的,仍然有一个“布尔”类型。
  • 在zval上,PHP引用再也不使用is_ref标识,而是用IS_REFERENCE类型。下一部分将会讨论。
  • IS_INDIRECT和IS_PTR是特殊的内部类型。

IS_LONG目前存储的是zend_long类型的值,而不是一个普通的C语言long整数。缘由是在64位windows(LLP64)上,long型只有32位,因而在windows上PHP5的IS_LONG老是32位的。在64位操做系统上,即便你使用的是windows,PHP7都容许你使用64位的数字。

zend_refcounted类型相关的细节将在下一部分讨论,如今咱们先看一下PHP引用的实现。

引用

PHP7处理PHP引用(&)的方式与PHP5彻底不一样(我能够告诉你这个改变是PHP7最大的bug来源之一)。PHP5中引用的实现以下:

一般,写时复制(COW)机制意味着在修改以前,zval要先进行分离,以保证不会把其它共用该zval的变量给一块儿修改了。这与值传递的语义相符。

对PHP引用来讲,就不是这种状况了。若是一个值是引用,那么修改的时候就但愿其它变量也同步被修改。PHP5用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])

这种设计一个很重大的问题就是不能在普通变量和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引用一般比普通变量更慢。下面的例子就有这个问题:

$array = range(0, 1000000);
$ref =& $array;
var_dump(count($array)); // <-- 这里发生zval分离

由于count()的参数是按值传递的,而$array是一个引用变量,在把它传递给count()时,会对该数组执行完整的复制。若是$array不是引用,它的值就能够共用,在传递的时候就不会发生复制。

如今来看下PHP7中引用的实现。因为zval再也不是独立分配,再也不可能使用PHP5同样的方式。转而增长了IS_REFERENCEl类型,它的值是以下的zend_reference结构:

struct _zend_reference {
    zend_refcounted   gc;
    zval              val;
};

因此zend_reference本质上只是一个有引用计数的zval。在一个引用集合中全部的变量都会保存一份IS_REFERENCEl类型的zval,而且指向同一个zend_reference实例。val跟其余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,该引用的引用计数是2(有两个变量用到了这个引用),可是值自己的引用计数是1(只有一个zend_reference指向了该值)。再考虑下引用变量和普通变量混合的状况:

$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) ---^
          // 注意全部的变量共享同一个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再也不是独立的堆分配且其自己再也不存储引用计数。转而是它们指向的复杂类型的值(如字符串、数组、对象)会存储引用计数。这一般会带来更少的内存分配、间接操做和内存使用。

下一部分将会讨论其它复杂类型。

相关文章
相关标签/搜索