原文: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 => "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 => "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 => "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