首页 > 代码库 > PHP写时复制

PHP写时复制

原文:http://www.huamanshu.com/blog/2014-05-18.html

起源

写时复制英文名字叫“copy-on-write”,简称“cow”,又名“靠”

今天查了下这个"cow",原来起源于*nix内存管理机制,后来广泛应用在其它开发语言上,C++的STL,还有PHP,相信很多人都了解这个写时复制,经常跟别人侃得甚欢。

$foo = ‘foo‘; $bar = $foo;

不会

初写这样的PHP代码时,常会问,这样的话会不会因为复制而占用更多内存,旁边会有人说,不会。当然你不知道他说的“不会”,是指“不占用”,还是“我不知道”。后来,了解到memory_get_usage()这函数后,亲自测试验证,确实没有成倍增加。

<?php var_dump(memory_get_usage()); $foo = ‘foooooooooooooooooooooooooooooo‘; var_dump(memory_get_usage()); $bar = $foo; var_dump(memory_get_usage()); ------ 结果 ------- int(121704) int(121840) // 与前者相差 echo $((840-704)) = 136 int(121888) // 与前者相差 echo $((888-840)) = 48

xdebug管中窥豹

神奇的是无论$foo变量多大,复制$foo$bar时,内存消耗只增加48,这48是什么东西呢?这个时候,得请xdebug来了解下,到底发生了什么事情。

<?php $foo = ‘foo‘; xdebug_debug_zval(‘foo‘); $bar = $foo; xdebug_debug_zval(‘foo‘); xdebug_debug_zval(‘bar‘); $bar = ‘bar‘; xdebug_debug_zval(‘foo‘); xdebug_debug_zval(‘bar‘); ------ 结果 -------     foo: (refcount=1, is_ref=0)=‘foo‘ // copy foo before foo: (refcount=2, is_ref=0)=‘foo‘ // copy foo after bar: (refcount=2, is_ref=0)=‘foo‘ // write bar befor foo: (refcount=1, is_ref=0)=‘foo‘ // write bar after bar: (refcount=1, is_ref=0)=‘bar‘ // write bar after

注意观察会发现refcount的值在copy foo和write bar的时候发生了变化,这过程就是PHP的写时复制。好吧,我这样说,跟高中老师说的,看这就是能量守恒定律一样无趣。

PHP变量结构体

其实refcount在PHP内核中,是变量存储的结构体的一个成员,完整名称refcount__gc,上述xdebug打印出来的is_ref也是变量结构体的一个成员is_ref__gc,查看它完整的定义可以查看PHP源码Zend/zend.h

struct _zval_struct {                                                                                                                                 /* Variable information */     zvalue_value value;   /* value */     zend_uint refcount__gc;     zend_uchar type;   /* active type */     zend_uchar is_ref__gc; };

refcount__gc是记录变量引用次数,当$bar = $oo的时候,foo的refcount加一,由于bar与foo是同指向一个变量,引用计数同为2。当bar值被改写时,发生了写时复制,这结构体指向的内在块已经不能同时支持两个值了,于是内核就做了复制一份foo出来给bar,并设置其值为bar。is_ref__gc是用来标识是否为引用的,也就是说当我们对变量进行引用的时候,它就派上用场了。

我们先来看PHP内核是如何创建为我们创建一个变量,复制变量和引用变量。PHP是一个弱类型变量语言,没有type的说法,但PHP是由C写的,C类型定义和使用有着严格的要求。

创建变量

<?php $foo = ‘FOO‘  // PHP内核创建$foo变量 zval *foovar; MAKE_STD_ZVAL(foovar); // 内核专门封装为变量申请内存空间的宏 ZVAL_STRING(fooval, "FOO", 1); // type => ZVAL_STRING, value =http://www.mamicode.com/>"FOO" ZEND_SET_SYMBOL(EG(active_symbol_table), "foo", foovar); // 把$foo加入当前作用域的符号表中,使之在后面可以在symbo_table中可以检索出$foo,也就是PHP代码中要使用$foo的时候。

现在看来我们简单的写了一个$foo = ‘FOO‘;,内核是这样来给我们做底层的事情,我们不用担心内存到底应该分配多少,还得释放,PHP是不是很人性,让PHP开发者轻松就入门来,看到PHP的世界。

复制变量

<?php $foo = ‘FOO‘; $bar = $foo;  // PHP内核创建$foo变量 zval *foovar; MAKE_STD_ZVAL(foovar); ZVAL_STRING(fooval, "FOO", 1); // type => ZVAL_STRING, value =http://www.mamicode.com/>"FOO" ZEND_SET_SYMBOL(EG(active_symbol_table), "foo", foovar); //  ZVAL_ADDREF(foovar); //显式的增加了foovar结构体的refcount__gc zend_hash_add(EG(active_symbol_table), "bar", sizeof("bar"),&foovar, sizeof(zval*), NULL);    

引用变量

<?php $foo = ‘FOO‘; $bar = &$foo;  // PHP内核创建$foo变量 zval *foovar; MAKE_STD_ZVAL(foovar); ZVAL_STRING(fooval, "FOO", 1); // type => ZVAL_STRING, value =http://www.mamicode.com/>"FOO" zend_hash_add(EG(active_symbol_table), "bar", sizeof("bar"),&foovar, sizeof(zval*), NULL);     zend_hash_add(EG(active_symbol_table), "bar", sizeof("bar"),&foovar, sizeof(zval*), NULL); // $bar、$foo同指向一个内在地址  

复制、引用多重组合

<?php $foo = ‘foo‘; xdebug_debug_zval(‘foo‘); $bar = $foo; xdebug_debug_zval(‘foo‘); xdebug_debug_zval(‘bar‘); // 增加一个引用 $hua = &$foo; xdebug_debug_zval(‘foo‘); xdebug_debug_zval(‘bar‘); xdebug_debug_zval(‘hua‘); ------ 结果 ------- 1.foo: (refcount=1, is_ref=0)=‘foo‘ // copy foo befor 2.foo: (refcount=2, is_ref=0)=‘foo‘ // copy foo after 3.bar: (refcount=2, is_ref=0)=‘foo‘  4.foo: (refcount=2, is_ref=1)=‘foo‘ // ref foo after 5.bar: (refcount=1, is_ref=0)=‘foo‘ 6.hua: (refcount=2, is_ref=1)=‘foo‘

都知道当引用的时候,改变foo,?hua两者其一值,另外一变量也跟着变化。总结起来就是

  • 当只有复制的时候,refcount++(看结果2)
  • 当只有引用的时候,is_ref会由0改写成1
  • 当一个变量既有复制也被引用时,引用与被引用refcount、is_ref一致,复制变量refcount=1, is_ref=0(看结果4-6)

写时复制、引用多重组合变态版

<?php $foo[‘hua‘] = ‘man‘; $bar  = &$foo[‘hua‘]; $huamanshu = $foo; $huamanshu[‘hua‘] = ‘shu‘; echo $foo[‘hua‘];

猜下结果是什么?都说了是变态版,怎么能按常识去想答案呢?正确答案是:‘shu‘。这个$huamanshu跟我们的$foo扯不上半毛钱关系啊?

<?php $foo [‘hua‘] = ‘man‘; xdebug_debug_zval(‘foo‘); $bar  = &$foo[‘hua‘]; xdebug_debug_zval(‘foo‘); xdebug_debug_zval(‘bar‘); $huamanshu = $foo; xdebug_debug_zval(‘foo‘); xdebug_debug_zval(‘huamanshu‘); $huamanshu[‘hua‘] = ‘shu‘; xdebug_debug_zval(‘foo‘); xdebug_debug_zval(‘huamanshu‘); xdebug_debug_zval(‘bar‘);  $ php y.php  foo: (refcount=1, is_ref=0)=array (‘hua‘ => (refcount=1, is_ref=0)=‘man‘) foo: (refcount=1, is_ref=0)=array (‘hua‘ => (refcount=2, is_ref=1)=‘man‘) bar: (refcount=2, is_ref=1)=‘man‘ foo: (refcount=2, is_ref=0)=array (‘hua‘ => (refcount=2, is_ref=1)=‘man‘) huamanshu: (refcount=2, is_ref=0)=array (‘hua‘ => (refcount=2, is_ref=1)=‘man‘) foo: (refcount=1, is_ref=0)=array (‘hua‘ => (refcount=3, is_ref=1)=‘shu‘) huamanshu: (refcount=1, is_ref=0)=array (‘hua‘ => (refcount=3, is_ref=1)=‘shu‘) bar: (refcount=3, is_ref=1)=‘shu‘

个人觉得$foo[‘hua‘]refcount的值在$huamanshu[‘hua‘] = ‘shu‘;的时候,应该由2变成0,而不是继续加一。

当然,会有不少的同学会犯这样的毛病:

$array = [ 1, 2, 3, 4 ];  foreach ($array as &$value) {} echo implode(‘,‘, $array), PHP_EOL;                                                                                                                // unset($value); 解决办法之一 foreach ($array as $value)  {} echo implode(‘,‘, $array), PHP_EOL;  // echo 1,2,3,4 1,2,3,3

是因为第一个foreach之后,$value的引用保持,后面涉及$value的时候,都会改变$value值,也就是$array的最后一个元素值。第二个foreach的每一次都在改变$value值,只是到了第三次就改写了$value值,第四次只是重复赋刚才第三次覆盖的值而已。所以,在优化PHP代码的时候,foreach最好写不一样的$v`,或者`$item`之类的名字,避免上面用引用而导致改写,实在不行就用unset掉用引计数。

其实,PHP很聪明的,当然每个语言都是,尤其是C,当之无愧!PHP内核还专门定义了8种类型来支持我们在PHP代码中创建的所有类型,太让我们变懒惰了。看完这个类型列表,会对PHP内核实现有更深的了解。

  • IS_NULL
    • 第一次使用的变量如果没有初始化过,则会自动的被赋予这个常量,当然我们也可以在PHP语言中通过null这个常量来给予变量null类型的值。
    • 这个类型的值只有一个 ,就是NULL,它和0与false是不同的。
  • IS_BOOL
    • 布尔类型的变量有两个值,true或者false。
    • 在PHP语言中,while、if等语句会自动的把表达式的值转成这个类型的。
  • IS_LONG
    • PHP语言中的整型,在内核中是通过所在操作系统的signed long数据类型来表示的。 在最常见的32位操作系统中,它可以存储从-2147483648 到 +2147483647范围内的任一整数
    • 如果PHP语言中的整型变量超出最大值或者最小值,它并不会直接溢出, 而是会被内核转换成IS_DOUBLE类型的值然后再参与计算
    • 因为使用了signed long来作为载体,所以这也就解释了为什么PHP语言中的整型数据都是带符号的了。
    • 例子:$a=2147483647; $a++; echo $a;//会正确的输出 2147483648;
  • IS_DOUBLE
    • PHP中的浮点数据是通过C语言中的signed double型变量来存储的, 这最终取决与所在操作系统的浮点型实现。
    • 我们做为程序猿,应该知道计算机是无法精准的表示浮点数的, 而是采用了科学计数法来保存某个精度的浮点数,用计算机来处理浮点数简直就是一场噩梦。
  • IS_STRING
    • PHP中最常用的数据类型——字符串,在内存中的存储和C差不多, 就是一块能够放下这个变量所有字符的内存,并且在这个变量的zval实现里会保存着指向这块内存的指针。
    • 与C不同的是,PHP内核还同时在zval结构里保存着这个字符串的实际长度, 这个设计使PHP可以在字符串中嵌入‘’字符,也使PHP的字符串是二进制安全的, 可以安全的存储二进制数据!本着艰苦朴素的作风,内核只会为字符串申请它长度+1的内存, 最后一个字节存储的是‘’字符,所以在不需要二进制安全操作的时候, 我们可以像通常C语言的方式那样来使用它。
  • IS_ARRAY
    • 数组是一个非常特殊的数据类型,它唯一的功能就是聚集别的变量。 在C语言中,一个数组只能承载一种类型的数据,而PHP语言中的数组则灵活的多, 它可以承载任意类型的数据,这一切都是HashTable的功劳。
    • 每个HashTable中的元素都有两部分组成:索引与值, 每个元素的值都是一个独立的zval(确切的说应该是指向某个zval的指针)。
  • IS_OBJECT
    • 和数组一样,对象也是用来存储复合数据的,但是与数组不同的是, 对象还需要保存以下信息:方法、访问权限、类常量以及其它的处理逻辑。
  • IS_RESOURCE
    • 有一些数据的内容可能无法直接呈现给PHP用户的, 比如与某台mysql服务器的链接,或者直接呈现出来也没有什么意义。 但用户还需要这类数据,因此PHP中提供了一种名为Resource(资源)的数据类型。

8种类型引自www.walu.cc

原文:http://www.huamanshu.com/blog/2014-05-18.html

PHP写时复制