PHP7变量内部实现(二)(译)

在上一篇文章中,讨论了PHP5和PHP7变量之间大的改变。回顾一下最大的变化就是zval不再单独分配,不再自己存储引用计数。简单类型比如整形、浮点型的值直接存在zval内部,复杂的类型还是通过一个指针指向独立的结构来表示。

复杂类型都有一个通用的头,就是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;
};

这个头保存了refcount,值的类型,周期回收信息(gc_info),以及和类型相关的flag信息。

下面会详细的讨论一下各种复杂类型的实现,以及和PHP5中的区别。其中引用类型在上一篇中已经说过了。资源类型这里不会提到,因为我觉得没什么意思。

String

PHP7中的字符串有一个专门的zend_string类型,是这么定义的:

struct _zend_string {
    zend_refcounted   gc;   /* 这就是上面说的通用的 zend_refcount gc */
    zend_ulong        h;        /* hash value */
    size_t            len;
    char              val[1];
};

除了引用计数的头,字符串结构还包含了一个哈希缓存h,长度len,值val
哈希缓存的作用是避免每次在hashtable中查找key的时候重复计算hash,在首次使用的时候就会被初始化成一个非0的哈希值。

如果你对C不是很熟悉的话,就会对val感到奇怪:一个char怎么能存一个字符串呢?这其实是用了" struct hack " ,数组只用一个元素来声明,在创建zend_string的时候,我们给它分配一个大的string。我们还是可以通过这个val元素来访问这个string。

当然,这是一个技术上没有定义的行为,因为我们通过一个单字符来读、写一个array,然而,C编译器不知道你这样乱整。C99支持这样使用,其实就是"flexible array members(柔性数组)"。

这种新的字符串类型比原生的C字符串有一些有点:第一,本身包含了字符串的长度,意味着取长度的时候不需要遍历了;第二,字符串本身包含了引用计数,可以在多个地方使用同一个字符串,而不是使用zval,这对于共享hashtable的key很重要。

新的字符串和C字符串相比也有一个很大的缺点:从 zend_ztring 可以很简单的用 str->val取得对应的C string,然而不能直接把C的string变成 zend_string,实际上需要新申明一个zend_string,然后把C的string复制进去,这点在C代码中处理字符串的时候不方便。

下面是string中的flags (在gc里面flags字段):

#define IS_STR_PERSISTENT           (1<<0) /* allocated using malloc */
#define IS_STR_INTERNED             (1<<1) /* interned string */
#define IS_STR_PERMANENT            (1<<2) /* interned string surviving request boundary */

Persistent strings 用的是系统的分配器而不是Zend内存管理器(ZMM),这样可以在不仅一次请求中有效,所以就可以透明的在zval中使用一个永久的字符串,然后这在PHP5中需要事先拷贝到ZMM中。

Interned strings 直到请求结束时才会被销毁,也不需要引用计数。他们也是不重复的,在创建一个新的 interned string的时候引擎会检查给的内容是不是已经存在了。PHP代码中所有的字符串(字符换、变量名、函数名等)一般用的是这个。

Permanent strings 是在请求开始时创建的interned string,但是请求结束时不会被销毁。

如果使用了 Opcache, interned string 就会存在共享内存里(SHM),在所有的PHP worker 进程间共享,这种情况下,Permanent strings 就没什么意思了,因为 Interned strings 会被销毁。

Array

因为上一篇文章已经说了数组了,所以这里不再展开讨论,但是因为最近的小改动影响了一些细节,但是大的概念还是一样的。

这里直说一个新概念:Immutable arrays(不可变数组),本质上和 interned string差不多,没有引用计数,在请求结束之前不会被销毁(或者更久)。

为了避免内存管理问题,不可变数组只在Opcache打开的时候使用,下面的代码可以看出会有什么差异:

for ($i = 0; $i < 1000000; ++$i) {
    $array[] = ['foo'];
}
var_dump(memory_get_usage());

有Opcache的情况下是 32M,没有的时候就会飙到390M,因为每个$array的元素都会拿到新的[foo]的拷贝。原因是VM为了避免SHM出错 ,而采用真的拷贝,而不是 refcount + 1。我希望将来可以在不用Opcache的时候解决这个灾难性的问题。

Objects in PHP 5

先看一下PHP5的对象是怎么工作的,找到里面低调的地方:zval 本身存着 zend_object_value,定义如下:

typedef struct _zend_object_value {
    zend_object_handle handle;
    const zend_object_handlers *handlers;
} zend_object_value;

handle是对象的唯一id,用来查找这个对象的数据,handlers 是一个存了对象里各种方法的指针的虚函数表。常规的PHP代码中,对象的 handler 表都是一样的,但是 扩展里面创建的对象,可以通过自定义 handlers 来改变对象的行为。

对象句柄就是一个对象库(object_store)里面的索引,对象库就是对象组成的数组,像下面这样:

typedef struct _zend_object_store_bucket {
    zend_bool destructor_called;
    zend_bool valid;
    zend_uchar apply_count;
    union _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 clone;
            const zend_object_handlers *handlers;
            zend_uint refcount;
            gc_root_buffer *buffered;
        } obj;
        struct {
            int next;
        } free_list;
    } bucket;
} zend_object_store_bucket;

这里的情况比较复杂。前3个成员是一些元信息(析构函数是否调用,bucket是否被使用,对象被递归调用了多少次),中间的联合体用来区分bucket当前是否正在使用还是在空闲列表中。对于使用者来说重要的是struct _store_object

第一个元素object是指向实际对象的指针,它不是直接内嵌在对象库桶里面的,因为对象没有一个固定的大小。这个指针下面紧跟着3个元素分别负责销毁、释放、克隆。注意,PHP里面对象的销毁和释放是不同的步骤,以前在某些情况下会跳过(不完全释放)。clone这个字段实际上从来没用过,因为这些字段都不是一般对象的一部分,(不管出于什么原因)他们都会被每个独立的对象拷贝,而不是共享。

接下来就是handlers指针,指向一个普通的对象,当在不知道zval的情况下销毁这个对象时,这个指针就会有用了(存对象的资源句柄)。

槽里面还包含了一个refcount,因为zval已经存了refcount了,这里还存一个就显得有点怪了。。。为什么需要这样呢?因为通常"拷贝" zval 的时候只是 refcount + 1,但是某些情况下,也会有真的拷贝,比如,申明一个全新的zval但是有相同zend_object_value,这种情况下就是两个独立的zval共享同一个对象库槽,所有对象库槽本身才会需要一个refcount。这种“双重引用计数”在PHP5的zval实现中就是硬伤。bufferd指针指向 GC root buffer,也是因为这个原因才重复的。

现在我们来看看 object 指向的实际 对象吧,用户用的对象通常是这样的:

typedef struct _zend_object {
    zend_class_entry *ce;
    HashTable *properties;
    zval **properties_table;
    HashTable *guards;
} zend_object;

zend_class_entry指向的是这个对象实例化对应的类本身,propertiesproperties_table 存的是对象的属性,动态属性(在运行的时候才有的,声明的时候没有)存在properties里。在类里面申明的属性有一个优化点:在编译期间,每个属性都被赋予了一个索引,索引和属性的值都存在properties_table里面。而属性名和索引的对应关系又存在类里面的一个hashtable里面。这样的哈希表内存开销的是避免单个对象,另外在运行时,属性的这个索引是动态缓存的。

guards这个哈希表是用来实现魔术方法的,比如__get,这里不讨论。

除了上面已经提到的“双重引用计数”,对象的实现也当占内存,仅有一个属性的类都会占用136bytes的内存。另外还有比如:从一个对象zval里面拿一个属性,先要取得对象库桶,然后再是 zend_object,然后再是properties_table,最后才是它指向的zval,这已经就有4层了(实际中,一般不会少于7层)。

Objects in PHP 7

PHP7尝试解决掉这些问题:去掉“双重引用计数”,减少内存占用,减少间接指向。先看一下新的zend_object:

struct _zend_object {
    zend_refcounted   gc;
    uint32_t          handle;
    zend_class_entry *ce;
    const zend_object_handlers *handlers;
    HashTable        *properties;
    zval              properties_table[1];
};

注意这个结构现在就只剩下一个对象了,zend_object_value被取而代之的是一个直接指向对象或者对象库的指针。

除了通用的zend_refcounted头,还可以看到handlehandles被移到zend_object里面了,另外properties_table也使用了 struct hack,所以zend_object和属性表将会分配在一块内存里面。当然,属性表现在直接存了zvals,而不是以前那样存的是指针。

guards现在没有直接体现在zend_object结构里面了,如果用到的话,它将会存在properties_table的第一个槽里面,当然,如果没有使用__get这样的魔术方法,guards将会被省略。

之前的dtor,free_storage,clone现在被移到了handlers里面:

struct _zend_object_handlers {
    /* offset of real object header (usually zero) */
    int                                     offset;
    /* general object functions */
    zend_object_free_obj_t                  free_obj;
    zend_object_dtor_obj_t                  dtor_obj;
    zend_object_clone_obj_t                 clone_obj;
    /* individual object functions */
    // ... rest is about the same in PHP 5
};

offset是和内部对象表示相关的,内部对象通常包含一个标准的zend_object,但通常也会添加一些附加的东西,在PHP5中是这么弄得:

struct custom_object {
    zend_object std;
    uint32_t something;
    // ...
};

就是说,可以简单的把zend_object变成自定义的struct custom_object*,这是在C上面进行结构的继承。然后再PHP7中这样做的话就会有问题,因为PHP7的zend_object用了 struct hack来存属性表,PHP会在zend_object的结尾存属性部分,这会导致覆盖掉这些额外的自定义信息,所以PHP7中应该写在前面:

struct custom_object {
    uint32_t something;
    // ...
    zend_object std;
};

这就意味着不能直接在zend_objectstruct custom_object*之间转换,因为offset不同,在编译的时候可以通过offsetof()宏来确定offset。

你也许会好奇为什么PHP7还是有一个handle,毕竟现在存了一个直接指向zend_object的指针,所以现在不需要用handle在对象库里面查找对象了。

然而handle还是需要的,因为对象库还是存在的,尽管已经精简了它。它现在只是一个简单的数组,存的是指向对象的指针。当创建一个对象的时候,就会有一个指针插入到这个对象库中,索引就是这个handle,当释放对象的时候,这个也会被移除。

为什么还需要对象库呢?原因就是在 请求结束 的时候,再运行用户空间的代码就不安全了,因为执行器已经部分关闭了。为了避免这种情况,PHP会请求结束的时候执行所有对象的析构函数并阻止他们之后再运行。所以才需要这么一个对象库列表。

另外,这个handle对调试代码有好处,因为他就是每个对象的唯一ID,所以就很方便查看两个对象是真的相同还是只是有相同的内容。HHVM尽管没有对象库的概念,但它还是存了对象的handle。

和PHP5相比,现在只有一个refcount了(zval本身没有了),内存占用变小了,只要40bytes存对象结构,16bytes存一个属性(包含它的zval)。间接指向也变少了,很多中间的结构都去掉了或者内嵌了。现在读一个属性只需要1步,而不是之前的4步了。

Indirect zvals

到此为此,常规的zval类型都说完了。还有两个在特定情况下才会出现的类型,都是PHP7新加的,其中一个就是 IS_INDIRECT.

间接zval意思就是zval的值是存在别的地方的,注意这和IS_REFERENCE这种直接指向另一个zval不同,zend_reference结构是直接嵌在zval里面的。

为了理解什么情况下这个是必须得,需要想一下PHP怎么实现一个变量的:

所有在编译时已知的变量都会被赋予一个索引,索引和值本身都会被 compiled variables(CV) table,PHP还允许你动态的引用变量,比如$$val。PHP将会为函数和脚本创建一个符号表,里面包含了所有的变量名和值得关系映射。

这就带来了一个问题:这两种访问格式怎么能同时支持呢?我们用CV表来访问常规的变量,用符号表来访问 $$ 变量。在PHP5中CV table用的是二级的zval**指针,指针会指向zval*的二级指针表,然后才会指向实际的zval:

+------ CV_ptr_ptr[0]
| +---- CV_ptr_ptr[1]
| | +-- CV_ptr_ptr[2]
| | |
| | +-> CV_ptr[0] --> some zval
| +---> CV_ptr[1] --> some zval
+-----> CV_ptr[2] --> some zval

当使用符号表的时候,zval*这个二级指针表实际是没有用的,zval**直接指向hashtable的桶,举例说明:

CV_ptr_ptr[0] --> SymbolTable["a"].pDataPtr --> some zval
CV_ptr_ptr[1] --> SymbolTable["b"].pDataPtr --> some zval
CV_ptr_ptr[2] --> SymbolTable["c"].pDataPtr --> some zval

PHP7中肯定不能用这样做了,因为当rehash hashtable的时候,指针就失效了。实际上PHP7用了相反的策略:对于存在CV table的变量,符号表里面就有一个INDIRECT入口,指向CV table的入口,CV table在符号表的有效期里是不会重新分配的,所以也就没有指针失效的问题。

所以如果在函数中使用 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_UNDEFzval,当hashtable没有关联的key的时候就会这样处理。所以如果unset($a) CV[0]写出UNDEF的时候,就和符号表里面没有"a"这个键差不多。

Constants and ASTs

PHP5和PHP7中都有IS_CONSTANTIS_CONSTANT_AST这两个特别的类型,这事干什么的?看下面这个例子:

function test($a = ANSWER,
              $b = ANSWER * ANSWER) {
    return $a + $b;
}

define('ANSWER', 42);
var_dump(test()); // int(42 + 42 * 42)

test()函数的两个参数默认值都用了ANSWER常量,但是在申明函数的时候常量并没有定义,只有等define()运行的时候,常量才可用。

出于这个原因,参数和属性的默认值(静态属性),常量以及其他接受静态表达式的东西,不得不推迟表达式的计算,直到首次使用。

如果值是常量或者静态属性,这些用的最平凡的地方都用了延迟计算,这个常量zval就有IS_CONSTANT标志。如果是常量表达式的话就是IS_CONSTANT_AST标示,并且zval指向了一个抽象语法树(AST)。

关于变量的实现,就说这么多吧,两篇文章了。不久之后再讲一些关于 VM的优化(尤其是 类型约定), 编译器的优化吧。

spinlock(自旋锁)和mutex(互斥锁)的区别(转)

首先spinlock是只有在内核态才有的,当然你也可以在用户态自己实现,但是如果想要调用spinlock_t类型,那只有内核态才有。但是semaphore是内核态和用户态都有的,mutex是一种特殊的semaphore。

spinlock是一种忙等待,也就是说,进程是不会睡眠的,只是一直在那里死循环。而mutex是睡等,也就是说,如果拿不到临界资源,那它会选择进程睡眠。那什么时候用spinlock,什么时候用mutex呢?首先,如果是在不允许睡眠的情况下,只能只用spinlock,比如中断的时候。然后如果临界区中执行代码的时间小于进程上下文切换的时间,那应该使用spinlock。反之应该使用mutex。

那mutex和semaphore有什么区别呢?mutex是用作互斥的,而semaphore是用作同步的。也就是说,mutex的初始化一定是为1,而semaphore可以是任意的数,所以如果使用mutex,那第一个进入临界区的进程一定可以执行,而其他的进程必须等待。而semaphore则不一定,如果一开始初始化为0,则所有进程都必须等待。同时mutex和semaphore还有一个区别是,获得mutex的进程必须亲自释放它,而semaphore则可以一个进程获得,另一个进程释放。

PHP7中变量的内部实现(一)(译)

PHP7中变量的内部实现(一)

最近的一边文章中提到的 提高hashtable的效率 已经被引入到PHP7,今天这篇文章简要描述下PHP新的变量形式。

由于内容很多,所以分为两部分:这部分说zval在PHP5和PHP7的不同,也会讨论一下引用的改进。第二部分会详细说一下类型的实现,比如字符串和对象。

PHP5 的 Zvals

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是一个能存任何变量的联合体:

typedef union _zvalue_value {
    long lval;                 // 布尔型 整形 资源
    double dval;               // 浮点型
    struct {                   // 字符串型
        char *val;
        int len;
    } str;
    HashTable *ht;             // 数组
    zend_object_value obj;     // 对象
    zend_ast *ast;             // 常量表达式
} zvalue_value;

C联合体在某一时刻只能有一个成员有效,并且联合体的大小就是这个成员的大小,所以节约内存。所有的联合成员会被存储在同一个地方,并且值是由你决定的。当你读lval成员的时候,值就是有符号的整形。当你读dval成员的时候,值就是一个双精度的浮点数.

想知道当前到底是什么类型,zval存了一个类型标示,这个type标示是用一个整数表示的:

#define IS_NULL     0      /* 空 */
#define IS_LONG     1      /* 长整型 */
#define IS_DOUBLE   2      /* 浮点型 */
#define IS_BOOL     3      /* 布尔型 */
#define IS_ARRAY    4      /* hashtable */
#define IS_OBJECT   5      /* 对象 */
#define IS_STRING   6      /* 字符串 */
#define IS_RESOURCE 7      /* 资源id 长整型 */

/* 延迟绑定常量 特殊类型*/
#define IS_CONSTANT 8
#define IS_CONSTANT_AST 9

PHP5中的引用计数

PHP5中的zval是分配在堆内存上的,PHP需要追踪哪些zval正在用,哪些zval需要释放,所以PHP才有引用计数的机制。zval中得refcount__gc存着被引用的次数。比如$a = $b = 42, 42就被两个变量引用了,所以refcount等于2。如果refcount等于0,那么表示这个值没有用了,可以释放了。

注意这里refcount说的引用和PHP引用(&)不是一回事。

和引用计数相关联的有一个“写时复制(copy on write)”的概念: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)

// 以下代码会导致写时复制
$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的refcount怎么变,这个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,u是一个联合体,所以这个指针可以指向两个不同的类型。buffered指针用来指向zval被引用的根缓冲区,所以如果在周期垃圾回收器回收之前变量被删除的话,这个指针也就没有了。next在垃圾回收器工作的时候起作用。

改变的动机

先讨论一下比特的大小(64位系统):首先,zvalue_value联合体有16个字节,因为strobj都是这么大,整个zval结构体是24字节,zval_gc_info是32字节。除了上面这些,在堆上申请一个zval的时候还需要额外的16字节。所以最终每个zval用了48字节-即使这个zval在很多都放都被使用了。

所以我们开始考虑为什么这个zval不高效。因为zval存整形的时候,整数本身占用8字节,此外,类型标示是必须的,即使很简单,但是也会填充到另外的8字节。

所以实际需要16字节来存储,再需要16字节来存引用计数和垃圾回收和16字节内存分配开销。更不用说我们真正的执行分配和释放,这都是高代价的操作。

这就产生了一个问题:一个简单的整形变量真的要这么复杂吗?引用计数、周期垃圾回收、堆分配值?答案当时是:否,因为这些都没什么卵用。

这里列举一下PHP5的zval存在的主要问题:

  • zval总是需要申请堆内存
  • zval总是有引用计数和周期垃圾回收信息,即使是在不值得共享的值(整形)和不能被循环的情况下。
  • 在zval是对象和资源时,直接引用计数会导致双倍引用计数。这背后的原因将在接下来的部分进行说明。
  • 一些情况会有大量的多重指针。比如访问一个变量中存储的对象时,会用到4级指针(这意味着连了个4个长度指针链)。这个在下部分再讨论。
  • 直接对zval引用计数意味着值只能在zval间共享,比如说,不可能在一个zval和一个hashtable的key之间共享一个字符串(在hashtable的key不是zval的情况下)。

PHP7 的 Zvals

PHP7带来了新的zval形式,其中最重要(根本)的改变就是zval不再是独立的堆分配内存了,也不再是自己带引用计数了。当然,复杂的值(比如字符串、数组、对象)可能会自己存储引用计数。所以有了以下优点:

  • 简单的值不需要分配也不需要引用计数了
  • 没有了双倍引用计数。对于对象来说,现在用的是对象内部计数了。
  • 由于现在引用计数是在值得内部,所以值的共享独立于zval结构了,一个字符串即可用于zval,也可以用于hashtable的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;                 // 哈希冲突链
        uint32_t cache_slot;           // 字面量缓存槽
        uint32_t lineno;               // 抽象语法树的行号
        uint32_t num_args;             // EX参数的个数
        uint32_t fe_pos;               // foreach 当前的位置
        uint32_t fe_iter_idx;          // foreach 迭代的 index
    } u2;
};

第一个成员还是一个value的联合体。第二个成员是存了一个类型信息的整数,这里用联合体分成了独立的字节(先忽略ZEND_ENDIAN_LOHI_4 宏,这是为了兼容各个字节顺序不同的平台)。这个子结构中重要的部分是type(和PHP5的差不多)和type_flags,这个接下来再详细说。

那么这里就有一个小问题:value是8个字节,由于结构填充加上一个字节也才16个字节。然而我们明显不需要8字节存类型。这就是为什么需要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;

    // 下面的有点特殊,暂时忽略
    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的大小是8字节而不是16了。只有整形lvaldval是直接存的值,其他所有的都是一个指针。所有的指针的类型用了引用计数并且有公共的头,这个是在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,flagsgc_info。这个type其实就是zval的类型,这就允许垃圾回收器区分不同的引用计数结构而不用存一个zval。flags在不同的类型中会有不同的作用,这个稍后再说。

这个gc_info和PHP5 zval的buffered差不多。然后,和以往存一个指向根缓冲区的指针不同的是现在包含了一个跟缓冲区的索引。因为根缓冲区现在能有10000个元素还包含了,所以用一个16位的数字就够了,而不用一个64位ude指针了。gc_info还对节点的“颜色”进行了编码,用来在回收的时候标记节点用。

Zval 内存管理

之前提到说zval不再单独申请堆内存了。但是他们总还是要存在某个地方吧,所以这里是怎么回事呢?尽管zvals大部分都是堆分配内存结构,但是现在是直接嵌入了。举例说:一个hashtable的bucket直接嵌入一个zval而不是存了一个指针指向zval。这就是说,一个对象的属性表和方法里面的已编辑变量表都会是以zval数组的形式存在一个块,而不是存了一堆指向独立的zval的指针。所以这些zvals就少了一级的指针,也就是以前是zval *,现在是zval.

当zval在一个新的地方使用时,在以前就意味着要复制一个zval *然后refcount加1。然而现在是复制zval的内容(忽略u2)并且它指向的refcount可能会加1(如果这个value用了引用计数的话)。

PHP是怎么知道一个值有没有引用过呢?这不能完全由类型决定,因为一些类型比如字符串和数组并不总是被引用啊。实际上zval的type_info成员决定着zval是否被引用了。这里把type其他的位的特征列出来:

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

type中主要的3个特征位是"refcounted","collectable"和"copyable"。refcounted啥意思应该知道了吧。collectable是说在一个周期内可用。比如说,字符串经常被应用,但是不能把字符串纳入一个周期。

copyable决定了在“复制”的时候是否需要拷贝。一个复制是一个硬拷贝,比如说,当要复制一个指向数组的zval时,这里不是简单的refcount+1。而是一个独立的数组会被新建。然后对于一些类型比如说对象和资源,一次复制就是refcount+1,这些类型被叫做non-copyable。这就是对象和资源的按值传递(不按引用传递)。

下面的这个表格展示了不同类型使用什么类型标示。“简单类型”指的是整形和布尔型,他们没有指向一个独立的结构。表中还有一列"immutable"(不可改变的),这个是用来标示不能改变的数组的,下部分再详细说。

Type           |refcounted |collectable|copyable   |immutable  
---------------+-----------+-----------+--------------------- 
simple types   |           |           |           |           
string         | x         |           | x         |           
intered 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就内建的而不是向堆内存申请的,所以现在内部使用=而不是->指针来表示啦。没有设置的变量的话,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) and zend_array_2 is destroyed
           // $b = zval_2(type=IS_ARRAY) -> zend_array_1(refcount=1, value=[])

这里每个变量依旧是一个独立的zval,但是两个zval都指向的同一个(引用计数的)zend_array结构。一旦有改变,这个数组就会被复制。这个模式和PHP5的相似。

类型

先见识一下PHP7中支持的类型吧:

// 普通类型
#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

// 常量表达式
#define IS_CONSTANT                 11
#define IS_CONSTANT_AST             12

// 内部类型
#define IS_INDIRECT                 15
#define IS_PTR                      17

这个列表和PHP5的也差不多,但是还是有几个新东西的:

  • IS_UNDEF被引入了,如果zval先前的指针是NULL的话,就会用这个(不要和IS_NULL搞混了,这两个没关系)。举个例子,在有引用计数的情况下,如果unset掉这个变量的话,那么就会被设置成IS_UNDEF
  • IS_BOOL被分开成IS_FALSEIS_TRUE。布尔变量的值就是这两,这个有助于基于类型检测的优化。这个变化对用户是透明的,因为这里其实还是只有一个boolean类型。
  • PHP的变量引用(不是引用计数)不再使用is_ref了,而是用了IS_REFERENCE类型来替代。这个是怎么工作的下次再说。
  • IS_INDIRECTIS_PTR类型是内部使用的。

IS_LONG类型现在用一个zend_long值而不是一个原生的C long型,这背后的原因是因为在64位的Windows上,long只有32bit,所以PHP5在Windows上用的都是32位的整数。PHP7允许使用64bit的数字(如果你是64位操作系统的话),即使你是Window系统。

zend_refcounted类型下部分再说。现在来看一下PHP的引用。

引用

PHP7引入了完全不同于PHP5的引用&机制(我现在可以告诉你这个变化是PHP7最大的bug来源之一)。先看一下PHP5的引用吧。

常规来说,写时复制原则意思是在改变一个zval的时候,zval就会被复制成独立的,为了不让改变这个共享值,所以都是按值传递的。

对于PHP来说这并不适用。如果有一个PHP的引用值,你想改变所有用这个值得变量。PHP5的zval中is_ref就决定着这个值是不是引用和是不是需要写时复制。举个例子:

$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 就不会去复制这个值
          

这种设计一个重要的问题就是不能在引用和非引用的变量间共享值。举个例子吧:

$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需要复制。
          // 现在 is_ref=0 和 is_ref=1 都是有同一个独立zval(2个)

$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)); // <-- 这里有一个复制

因为count()接受一个按值传递的值,但是$array是一个引用了,所以在传给count()之前就有一个全量的拷贝。如果$array不是一个引用的话,那么这个值就是共享的。

现在转到PHP7的引用实现。由于zval不再是独立申请的,所以PHP5中的方法就不能用了。取而代之的是IS_REFERENCE类型被引入啦,它的结构是这样的:

struct _zend_reference {
    zend_refcounted   gc;
    zval              val;
};

所以zend_reference本质上是一个有引用计数的zval。所有的引用变量zval,都有一个IS_REFERENCE指向同一个zend_reference实例。这个zval val和其他的zval是一样的,它也可以共享它指向的任何复杂的值。也即是说可以在一个普通变量和一个引用变量共享一个数组了。

再来看一下代码,这次再看PHP的语法。为了简单起见,这里就只展示大概:

$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。注意这里的refcount变成2了(因为这个引用里面有两个变量),但是值本身的refcount还是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) ---^
          // 4个变量都共享一个值 即使有引用存在

$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(),他不会被复制。但是PHP引用依旧比其他常规的变量耗时,因为他需要新建一个zend_reference结构,并且指向他,并且通常不能在快速机器码中处理。

结束语

总结一下,PHP7中最大的改变就是zval不在单独从堆中分配,也不在自己存refcount了。但是复杂的类型(比如字符串,数组,对象)还是自己存了引用数量。这个会使PHP减少申请内存,减少指针层级,减少内存使用。

PS:

[原文链接](http://nikic.github.io/2015/05/05/Internal-value-representation-in-PHP-7-part-1.html )