Press "Enter" to skip to content

Understanding PHP 7 Internals: References

The problem

In the previous chapter I said that a reference (REFERENCE) was a flag bit in PHP 5, whereas from PHP 7 on we turned it into a new type: IS_REFERENCE. References, however, are a very common thing to use, so this change brought a lot of knock-on changes — and during PHP 7 development it caused us quite a few bugs, because sometimes, through oversight, we forgot to handle this type.

The simplest case is that, when handling the various types, from now on we have to take this new type into account. For example, in PHP 7 this kind of code became very common:

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;
}

If you write your own extension and forget to account for this new type, it will cause problems.

Why?

So, given that this new type brings so many problems, why did we turn references into a type in the first place? Why not keep using a flag bit?
In one sentence: we had no choice. -_#
As mentioned earlier, the HashTable now stores the zval directly. So in the symbol table, how can two zval share a single value? For complex types like strings it's not so bad — we could seemingly add a flag bit in the zend_refcounted struct to mark a reference and be done with it. But that would still run into the copying caused by Change On Write. And we know that in PHP 7 some types are stored directly in the zval, such as IS_LONG; yet the reference type needs refcounting. So how do you represent a zval that is both IS_LONG and IS_REFERENCE?
To solve this, we created the new type:

As shown, a reference is a new type: zend_reference. For a zval of type IS_REFERENCE, zval.value.ref is a pointer to a zend_reference, which holds a refcount and a zval; the actual zval value is stored in zval.value.ref->val.
So for a reference to an IS_LONG, you use a zval of type IS_REFERENCE that points to a zend_reference, and inside that zend_reference->val sits a zval of type IS_LONG.

Change On Write

PHP uses reference counting for simple garbage collection. Consider the following code:

<?php
1. $val = "laruence";
2. $ref = &$val;
3. $copy = $val;
?>

$ref and $val are references pointing to the same zval. In PHP 5 we represented this situation with a refcount of 2 and a reference flag bit of 1. When $val is copied to $copy (line 3), we find that $val is a reference with a count greater than 1, so a Change on Write — that is, a separation — has to happen. So we need to copy this zval.
In PHP 7 the situation becomes much simpler. First, when the reference is assigned to $ref (line 2), an IS_REFERENCE type is created, and because two variables now reference it, the refcount of the zend_reference struct, zval.value.ref->gc.refcount, is 2.
Then, in the subsequent assignment to $copy (line 3), we find that $val is a reference, so we make $copy point at zval.value.ref->val — that is, the zval whose string value is laruence — and then increment that zval's refcount by 1, so zval.value.ref->val.value.str.gc.refcount becomes 2. No copy is produced.
This neatly solves the classic PHP 5 problem described in the previous chapter. For example, if we run that example from the previous chapter under PHP 7, we get:

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

As you can see, no copy actually occurred, so there's no performance issue at all.

Be First to Comment

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.