1. 问题

上一章说过引用(Reference)在 PHP5 的时候是一个标志位,而在 PHP7 以后我们把它变成一种新的类型:IS_REFERENCE。然而引用是一种很常见的应用,所以这个变化带来了很多的变化,也给我们在做 PHP7 开发的时候,因为有的时候疏忽忘了处理这个类型,而带来不少的 bug。

最简单的情况,就是在处理各种类型的时候,从此以后我们要多考虑这种新的类型。比如在 PHP7 中,这样的代码形式就变得很常见了:

try_again:
swtich (Z_TYPE_P(zv)) {
    case IS_TRING:
    break;
    case IS_ARRAY:
    break;
    ...
    case IS_REFERENCE:
    zv = Z_REFVAL_P(zv); //解引用
    goto try_again;
    break;
}

如果大家自己写的扩展,忘了考虑这种新的类型,那么就会导致问题。

2. 为什么

那么既然这种新类型会带来这么多问题,当时为什么要把引用变成一种类型呢?为什么不继续使用一个标志位呢?

一句话来说,就是我们不得不这么做。

前面说到,Hashtable 直接存储的是 zval,这样在符号表中,两个 zval 如何共用一个数值呢?对于字符串等复杂类型来说还好,我们貌似可以在 zend_refcounted 结构中加入一个标志位表明是引用来解决,然而这个也会遇到 Change On Write 带来的复制。但是我们知道在 PHP7 中,一些类型是直接存储在 zval 中的,比如 IS_LONG,但引用类型是需要引用计数的,那么对于一个是 IS_LONG 并且又是 IS_REFERENCE 的 zval 该如何表示呢?

为此,我们创造了这个新的类型:

如图所示,引用是一种新的类型:zend_reference,对于 IS_REFERENCE 类型的 zval,zval.value.ref 是一个指向 zend_reference 的指针,它包含了引用计数和一个 zval,具体的 zval 的值是存在 zval.value.ref->val 中的。

所以对于 IS_LONG 的引用来说,就是一个类型是 IS_REFERENCE 的 zval,它指向一个 zend_reference,而这个 zend_reference->val 中就是一个类型为 IS_LONG 的 zval。

3. Change On Write

PHP 采用引用计数来做简单的垃圾回收,考虑如下代码:

$val = 'foo';
$ref = &$val;
$copy = $val;

$ref$val 是指向同一个 zval 的引用,在 PHP5 的时候,我们通过一个引用计数为 2,并且引用标志位为 1 来表示这种情况,当把 $val 赋值给 $copy 的时候,我们发现 $val 是一个计数大于 1 的引用,所以要产生 Change on write,也就是分离,即需要复制这个 zval。

而在 PHP7 中,情况就变得简单了很多。首先在引用赋值给 $ref 的时候,生成一个 IS_REFERENCE 类型,然后因为此时有两个变量引用它,所以 zend_reference 这个结构的引用计数 zval.value.ref->gc.refcount 为 2。

在随后赋值给 $copy 的时候,发现 $val 是一个引用,于是让 $copy 指向的是 zval.value.ref->val,也就是字符串值为 foo 的 zval,然后把 zval 的引用计数 +1,也就是 zval.value.ref->val.value.str.gc.refcount 为 2,并没有产生复制。

这就很好的解决了上一章所说的 PHP5 的那个经典的问题,比如我们在 PHP7 下运行上一章的那个问题,得到的结果是:

$ php-7.0/sapi/cli/php /tmp/1.php
Used 0.00021380008539
Used 0.00020173048281

可见确实没有发生复制,从而不会产生任何性能问题。

原文链接