PHP变量写时复制和写时改变的理解

2400阅读 1评论2014-12-15 gipsa02
分类:PHP

// PHP 变量的 写时复制(cow) 和 写时改变(cow) 笔记
// since: 2014-12-15
// liuzhengyi
// ref:


zval简介


    PHP是一个弱类型的语言,所有的数据类型,在PHP内部都用zval这个数据结构表示.
    zval的结构如下:
    typedef struct _zval_struct {
        zvalue_value value;     // 存储真实的数据值, 是一个结构体
        zvalue_uint ref_count;  // 引用计数
        zvalue_uchar type;      // 变量类型
        zvalue_uchar is_ref;    // 是否是一个引用变量(&)
    } zval;

    debug_zval_dump() 方法可以dump出变量的一些zval信息.如:
        $a = 'hi';
        debug_zval_dump($a);
        // string(2) "hi" refcount(2)

    这里输出的refcount就是该变量的zval的引用计数.

    PHP引擎为了节约内存使用,会尽量重用zval:

        当执行 $b = $a; 时,
        PHP并不会重新分配内存,生成一个新的zval. 而是会在符号表中增加一条记录,让$b和$a共用一个zval.
        同时将这个zval的引用计数增加1.

        当执行 $b=&$a; 时,$b和$a也是共用一个zval,引用计数也会增加.
        但是$b和$a互为引用(使用同一个zval,并且值联动改变),为了记录这个信息,zval的is_ref会置为1.

    注意, 当 $b = $a时, 两者的值不会联动改变.
    而当 $b = &$a 时, 两者的值会联动改变.
    PHP是通过判断 zval中的ref_count和is_ref的值来做到这一点的.


写时复制

    考虑如下代码:
        $a = 'hi';
        $b = $a;
        $b = 'hello';

    第一行将生成一个zval,存储内容为'hi', ref_count=1, is_ref=0.
    第二行不会生成新的zval, $b和$a共用一个zval, 同时zval的 ref_count自增1, is_ref不变(为0).
    第三行的时候,会发生什么呢?
        在对$b进行写(赋值)时,PHP检查其zval的ref_count和is_ref.
        当发现其ref_count>1,is_ref=0时,就知道了$b和其他变量共用一个zval,并且其值*不应该*和共用者联动变化.
        于是,PHP会新生成一个zval,存储$b的值"hello".
        $b之前和$a共用的zval,现在不在和$b有任何关系了.
        我们就说$b从之前的zval中分离出来了,之前的zval的ref_count会自减1.

        这就是写时复制,当对共用zval的非引用变量进行赋值(写)时,PHP会复制一份zval出来.


写时改变

    考虑如下代码:
        $a = 'hi';
        $b = &$a;
        $b = 'hello';

    第一行将生成一个zval,存储内容为'hi', ref_count=1, is_ref=0.
    第二行不会生成新的zval, $b和$a共用一个zval, 同时zval的 ref_count自增1, is_ref也*置为1*.
    第三行的时候,对$b重新赋值,$a的值也应该改变,这又是如何做到的呢?
        同样, 在对$b进行写(赋值)时,PHP检查其zval的ref_count和is_ref.
        当发现其ref_count>1,is_ref=1时,就知道了$b和其他变量共用一个zval,并且其值*应该*和共用者联动变化.

        于是,PHP不会对$b进行分离,而是直接对应zval的值,于是共用这个zval的$a的内容也发生了变化.
        此即为写时改变.


两种复合情况

    考虑如下代码:
        $a = 'hi';
        $b = &$a;
        $c = $b;
    第一行将生成一个zval,存储内容为'hi', ref_count=1, is_ref=0.
    第二行不会生成新的zval, $b和$a共用一个zval, 同时zval的 ref_count自增1, is_ref也*置为1*.
    第三行的时候,将$b赋值给$c.此时会发生什么?
        $c 获得 $b的值的拷贝,但不会和$a,$b建立引用关系.
        所以无法和$b共用zval(如果共用zval,则无法表示$a和$b互为引用,而和$c不是引用关系).
        于是,会新生成一个zval,供$c使用,ref_count=1, is_ref=0.
        而$a 和 $b 还是共用之前的zval.

    再考虑如下代码:
        $a = 'hi';
        $b = $a;
        $c = &$b;
    第一行将生成一个zval,存储内容为'hi', ref_count=1, is_ref=0.
    第二行不会生成新的zval, $b和$a共用一个zval, 同时zval的 ref_count自增1, is_ref不变,为0.
    第三行的时候,将$c设置为$b的引用.此时又会发生什么?
        $b和$c互为引用,而和$a没有引用关系,一个zval无法表示,还是需要新生成一个zval.
        本来$a和$b共用一个zval,现在$b和$c要互为引用,必须共用一个zval,所以需要将$b和$a分离开.

        所以,这里先将$b和$a分离,生成一个新的zval,然后将$b和$c绑定到一个zval上,并将is_ref设为1.


ref_count和is_ref的另外一种情况

    前面说的几种情况都能对应到:
        ref_count>1 && is_ref=0
        ref_count>1 && is_ref=1
    剩下的情况呢,就是 ref_count<2.
    ref_count<2,说明这个变量独立使用一个zval,修改时不需要考虑其他变量.
    其实就是这种情况:
        $a = 'hi';
        $a = 'hello';
    没有分离,也没有复制.


debug_zval_dump()的输出

    前面提到:
        $a = 'hi';
        debug_zval_dump($a);
    会输出 ref_count = 2.

    但是当执行:
        $b = &$a;
        debug_zval_dump($b);
    则会输出 ref_count = 1.

    这是为什么呢?

    PHP中简单变量作为函数参数传递时,是按值传递的,即会传递变量的一个复制.
    所以将一个独占zval的变量传递给debug_zval_dump()时,传参时的复制操作使该zval的ref_count增加了1.
    而将一个共用引用zval(is_ref=1)的变量传递给debug_zval_dump()时,传参时的复制操作导致变量分离.
    最终导致传入的变量的zval的ref_count=1.


笔记一篇,欢迎批评讨论!
上一篇:linux下 如何直接覆盖目录
下一篇:shell获取分钟值的个位数

文章评论