六、 写复制(Copy on Write)
通过refcounting来节约内存的确是不错的主意,但是,当你仅想改变其中一个变量的值时情况会如何呢?为此,请考虑下面的代码片断:
<?php
$a = 1;
$b = $a;
$b += 5;
?>
通过上面的逻辑流程,你当然知道$a的值仍然等于1,而$b的值最后将是6。并且此时,你还知道,Zend在尽力节省内存-通过使$a和$b都引用相同的zval(见第二行代码)。那么,当执行到第三行并且必须改变$b变量的值时,会发生什么情况呢?
回答是,Zend要查看refcount的值,并且确保在它的值大于1时对之进行分离。在Zend引擎中,分离是破坏一个引用对的过程,正好与你刚才看到的过程相反:
zval *get_var_and_separate(char *varname, int varname_len TSRMLS_DC)
{
zval **varval, *varcopy;
if (zend_hash_find(EG(active_symbol_table),varname, varname_len + 1, (void**)&varval) == FAILURE) {
/* 变量根本并不存在-失败而导致退出*/
return NULL;
}
if ((*varval)->refcount < 2) {
/* varname是唯一的实际引用,
*不需要进行分离
*/
return *varval;
}
/* 否则,再复制一份zval*的值*/
MAKE_STD_ZVAL(varcopy);
varcopy = *varval;
/* 复制任何在zval*内的已分配的结构*/
zval_copy_ctor(varcopy);
/*删除旧版本的varname
*这将减少该过程中varval的refcount的值
*/
zend_hash_del(EG(active_symbol_table), varname, varname_len + 1);
/*初始化新创建的值的引用计数,并把它依附到
* varname变量
*/
varcopy->refcount = 1;
varcopy->is_ref = 0;
zend_hash_add(EG(active_symbol_table), varname, varname_len + 1,&varcopy, sizeof(zval*), NULL);
/*返回新的zval* */
return varcopy;
}
现在,既然引擎有一个仅为变量$b所拥有的zval*(引擎能知道这一点),所以它能够把这个值转换成一个long型值并根据脚本的请求给它增加5。
七、 写改变(change-on-write)
引用计数概念的引入还导致了一个新的数据操作可能性,其形式从用户空间脚本管理器看来与"引用"有一定关系。请考虑下列的用户空间代码片断:
<?php
$a = 1;
$b = &$a;
$b += 5;
?>
在上面的PHP代码中,你能看出$a的值现在为6,尽管它一开始为1并且从未(直接)发生变化。之所以会发生这种情况是因为当引擎开始把$b的值增加5时,它注意到$b是一个对$a的引用并且认为"我可以改变该值而不必分离它,因为我想使所有的引用变量都能看到这一改变"。
但是,引擎是如何知道的呢?很简单,它只要查看一下zval结构的第四个和最后一个元素(is_ref)即可。这是一个简单的开/关位,它定义了该值是否实际上是一个用户空间风格引用集的一部分。在前面的代码片断中,当执行第一行时,为$a创建的值得到一个refcount为1,还有一个is_ref值为0,因为它仅为一个变量($a)所拥有并且没有其它变量对它产生写引用改变。在第二行,这个值的refcount元素被增加为2,除了这次is_ref元素被置为1之外(因为脚本中包含了一个"&"符号以指示是完全引用)。
最后,在第三行,引擎再一次取出与变量$b相关的值并且检查是否有必要进行分离。这一次该值没有被分离,因为前面没有包括一个检查。下面是get_var_and_separate()函数中与refcount检查有关的部分代码:
if ((*varval)->is_ref || (*varval)->refcount < 2) {
/* varname是唯一的实际引用,
* 或者它是对其它变量的一个完全引用
*任何一种方式:都没有进行分离
*/
return *varval;
}
这一次,尽管refcount为2,却没有实现分离,因为这个值是一个完全引用。引擎能够自由地修改它而不必关心其它变量值的变化。
八、 分离问题
尽管已经存在上面讨论到的复制和引用技术,但是还存在一些不能通过is_ref和refcount操作来解决的问题。请考虑下面这个PHP代码块:
<?php
$a = 1;
$b = $a;
$c = &$a;
?>
在此,你有一个需要与三个不同的变量相关联的值。其中,两个变量是使用了"change-on-write"完全引用方式,而第三个变量处于一种可分离的"copy-on-write"(写复制)上下文中。如果仅使用is_ref和refcount来描述这种关系,有哪些值能够工作呢?
回答是:没有一个能工作。在这种情况下,这个值必须被复制到两个分离的zval*中,尽管两者都包含完全相同的数据(见图2)。
图2.引用时强制分离
同样,下列代码块将引起相同的冲突并且强迫该值分离出一个副本(见图3)。
图3.复制时强制分离