Press "Enter" to skip to content

PHP Internals — Variable Separation and References

In earlier articles I've already covered the internal representation of PHP variables (Understanding PHP Internals — Variables inside PHP), as well as how scoping is implemented in PHP (Understanding PHP Internals — Scope inside PHP). In this post we'll pick up where those left off and look at the concepts of variable separation and references in PHP:

First, let's recap the structure of zval:

struct _zval_struct {
        /* Variable information */
        zvalue_value value;             /* value */
        zend_uint refcount;
        zend_uchar type;        /* active type */
        zend_uchar is_ref;
};

We've never discussed the refcount and is_ref fields. As we know, PHP is a long-running server-side script interpreter, and for it efficiency and resource consumption are very important measures — which means PHP has to keep its memory usage as low as possible. Consider the following code:

<?php
   $var = "laruence";
   $var_dup = $var;
   unset($var);
?>

The first line creates a string variable, allocating 9 bytes of memory to hold the string "laruence" plus a trailing NULL () terminator.
The second line defines a new string variable and "copies" the value of the variable var into this new variable.
The third line unsets the variable var.
Code like this is very common in our everyday scripts. If PHP reallocated memory and copied the data on every variable assignment, the snippet above would need 18 bytes of memory — yet it's easy to see that there's really no need to allocate two separate blocks at all. Ha, the PHP developers saw that too:
As we said before, a PHP variable is implemented as a symbol name stored in the symbol_table that maps to a zval. For the first line above, for instance, a value "var" is stored in the symbol_table along with a pointer to a zval structure, and the variable's value "laruence" is kept inside that zval. So it's not hard to imagine that, for the code above, we could simply have the pointers behind "var" and "var_dup" point at the very same zval.
That is exactly what PHP does, and this is where the refcount field of the zval structure — which we've never introduced before — comes in.
refcount, as the name suggests, records how many references there currently are to the zval.
For example, for this code:

<?php
   $var = 1;
   $var_dup = $var;
?>

The first line creates an integer variable whose value is 1. At this point the zval holding the integer 1 has a refcount of 1.
The second line creates a new integer variable that also points to the zval just created, and increments that zval's refcount by 1, so the zval's refcount is now 2.
PHP provides a function that helps us observe this process, debug_zval_dump:

<?php
 $var = 1;
 debug_zval_dump($var);
 $var_dup = $var;
 debug_zval_dump($var);
?>

Output:

long(1) refcount(2)
long(1) refcount(3)

If you're puzzled — shouldn't var's refcount be 1?
As we know, for simple variables PHP passes arguments by value. That is, when debug_zval_dump($var) executes, $var is passed to debug_zval_dump by value, which bumps var's refcount by 1. So it's enough for us to see the fact that assigning a variable to another variable increments the zval's refcount.
Now let's go back to the code at the beginning of the article. What happens after the last line, unset($var), executes? Right — refcount is decremented by 1. Here's the code:

<?php
   $var = "laruence";
   $var_dup = $var;
   unset($var);
   debug_zval_dump($var_dup);
?>

Output:

string(8) "laruence" refcount(2)

But what about the following code?

<?php
   $var = "laruence";
   $var_dup = $var;
   $var = 1;
?>

Clearly, after this code runs, $var_dup should still be "laruence". So how is that achieved?
This is PHP's copy on write mechanism:
Before modifying a variable, PHP first checks that variable's refcount. If the refcount is greater than 1, PHP runs a separation routine. For the code above, when execution reaches the third line, PHP finds that the refcount of the zval that $var points to is greater than 1, so it copies out a brand new zval, decrements the original zval's refcount, and updates the symbol_table so that $var and $var_dup are separated (Separation). This mechanism is what's called copy on write.
Let's test it with code:

<?php
   $var = "laruence";
   $var_dup = $var;
   $var = 1;
   debug_zval_dump($var);
   debug_zval_dump($var_dup);
?>

Output:

long(1) refcount(2)
string(8) "laruence" refcount(2)

Now we know that when a variable is copied, PHP doesn't really duplicate it internally — it points to the same structure to save as much overhead as possible. So then, how are references in PHP implemented?

<?php
   $var = "laruence";
   $var_ref = &$var;
   $var_ref = 1;
?>

After this code finishes, $var will also have been indirectly modified to 1. This process is called change on write. So how does the ZE know that this copy doesn't need Separation?
That's where the is_ref field in the zval comes in:
For the code above, after the second line executes, the refcount of the zval that $var represents becomes 2, and at the same time is_ref is set to 1.
By the third line, PHP first checks the is_ref field of the zval that var_ref represents; if it is 1, no separation happens. The rough logic looks like this:

 if((*val)->is_ref || (*val)->refcount<2){
		// no Separation
        ... ;//process
  }

But then another question comes up: what about the following code?

<?php
   $var = "laruence";
   $var_dup = $var;
   $var_ref = &$var;
?>

For the code above there is a pair of copy on write variables, $var and $var_dup, and also a change on write pair, $var and $var_ref. How does this situation work out?
When the second line executes, just as described earlier, $var_dup and $var point to the same zval, with a refcount of 2.
When the third line executes, PHP finds that the zval it is about to operate on has a refcount greater than 1, so PHP performs Separation, splitting $var_dup off, and associates $var and $var_ref through change on write. That is, refcount=2, is_ref=1;
Based on this analysis, we can make debug_zval_dump report a refcount of 1:

<?php
	$var = "laruence";
    $var_dup = &$var;
	debug_zval_dump($var);
?>

Output:

string(8) "laruence" refcount(1)

As for the detailed reason, a little analysis on your part will get you there — I won't spell it out for you. 😉
This time we've covered PHP's variable separation mechanism. Next time I'll move on to how to receive and pass out parameters from PHP scripts inside an extension. Also, since things have been quite eventful for me lately (I changed jobs), apologies for taking so long to post an update.

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.