PHP7 使用了和 PHP5 中完全不同的方法来处理 PHP & 符号引用的问题(这个改动也是 PHP7 开发过程中大量 bug 的根源)。我们先从 PHP5 中 PHP 引用的实现方式说起。

通常情况下, 写时复制原则意味着当你修改一个 zval 之前需要对其进行分离来保证始终修改的只是某一个 PHP 变量的值。这就是传值调用的含义。

但是使用 PHP 引用时这条规则就不适用了。如果一个 PHP 变量是 PHP 引用,就意味着你想要在将多个 PHP 变量指向同一个值。PHP5 中的 is_ref 标记就是用来注明一个 PHP 变量是不是 PHP 引用,在修改时需不需要进行分离的。比如:

<?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 引用变量和 PHP 非引用变量之间共享同一个值。

比如下面这种情况:

<?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 中使用引用比普通的值要慢。比如下面这个例子:

<?php

$array= range(0, 1000000);

$ref=& $array;

var_dump(count($array)); // <-- 这里会进行分离

 

因为 count() 只接受传值调用,但是 $array 是一个 PHP 引用,所以 count() 在执行之前实际上会有一个对数组进行完整的复制的过程。如果 $array 不是引用,这种情况就不会发生了。

现在我们来看看 PHP7 中 PHP 引用的实现。因为 zval 不再单独分配内存,也就没办法再使用和 PHP5 中相同的实现了。

所以增加了一个 IS_REFERENCE 类型,并且专门使用 zend_reference 来存储引用值:

struct _zend_reference {

 zend_refcounted gc;

 zval    val;

};

 

本质上 zend_reference 只是增加了引用计数的 zval。所有引用变量都会存储一个 zval 指针并且被标记为 IS_REFERENCE。val 和其他的 zval 的行为一样,尤其是它也可以在共享其所存储的复杂变量的指针,比如数组可以在引用变量和值变量之间共享。

我们还是看例子,这次是 PHP7 中的语义。

为了简洁明了这里不再单独写出 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,注意它的引用计数是 2(因为有两个变量在使用这个 PHP 引用)。但是值本身的引用计数是 1(因为 zend_reference 只是有一个指针指向它)。

下面看看引用和非引用混合的情况:

<?php

$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, 即使有的是 PHP 引用有的不是

$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 最大的不同就是所有的变量都可以共享同一个数组,即使有的是 PHP 引用有的不是。

只有当其中某一部分被修改的时候才会对数组进行分离。

这也意味着使用 count() 时即使给其传递一个很大的引用数组也是安全的,不会再进行复制。

不过引用仍然会比普通的数值慢,因为存在需要为 zend_reference 结构体分配内存(间接)并且引擎本身处理这一块儿也不快的的原因。

 

结语

总结一下 PHP7 中最重要的改变就是 zval 不再单独从堆上分配内存并且不自己存储引用计数。

需要使用 zval 指针的复杂类型(比如字符串、数组和对象)会自己存储引用计数。

这样就可以有更少的内存分配操作、更少的间接指针使用以及更少的内存分配。