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 => "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

时间: 2024-10-20 00:22:22

PHP写时复制的相关文章

PHP &quot;数组变量&quot;之&quot;写时复制的要点&quot; 只有数组才有的概念。

1.如果数组指针位置非法,复制时,会将新数组指针初始化! 2.值传递时,PHP采用了一个COW(写时复制,copy on write)的优化措施! 写时复制的两个要点: <?php $arr1 = array('吕布','赵云','典韦'); end($arr1);next($arr1); //非法了 $arr2 = $arr1; //复制数组////var_dump($arr2);echo '<br>';var_dump(current($arr2));//初始化$arr2      

Ring3下绕过Windows写时复制机制实现全局EAT钩子

在注入到某进程中对Ntdll下EAT钩子的时候作用域仅仅只是当前进程,可是明明所有进程的Ntdll模块全是映射的同一个啊.原来Windows支持一种机制,允许两个或两个以上的进程共享同一块存储器.不过操作系统会给共享的存储页指定写时复制属性,当有个进程想修改一个共享页面时,操作系统会从内存中找到一个闲置页面并将修改的页面内容复制到这个闲置页面上,再将虚拟地址空间和这个新的页面映射上.那么只需在下钩前先想办法消掉页面的写时复制属性,再hook ntdll的时候就能在Ring3下实现类似Ring0钩

php变量之写时复制机制(copy on write)

编程思想虽然可以共用,不过语言间的差异还是比较明显的,只是使用者之间没有意识到而己,而了解其中的差异对于编写程序以及把握性能还是有好处的.下面我们来介绍下PHP的一个很重要的机制copy on write,我们先以最简单的变量来介绍这个机制,在说这个之前,笔者先来介绍下弱类型是怎么实现的. 大家都知道,PHP是由C实现的,可是C是强类型语言,PHP怎么做到弱类型语言.一起来看下,PHP变量在C语言低层中的代码, typedef struct _zval_struct zval; typedef

php底层--4 写时复制

变量的赋值与引用 例如:$a=3; $b=$a; 这个时候是否就产生了2个结构体呢? No,如果是的话,这两个结构体的type,value全都一样,很浪费呀,所以在PHP实现的时候并没有copy一个结构体出来,而是$a,$b共用一个结构体. 在传值赋值时,并没有新生结构体,而是共用的. $a=3; 产生一个结构体 zvalue:3; type:IS_LONG; refcount_gc:1; is_ref_gc:0; $b=$a; 这个时候并没有新产生一个结构体,而是原来的结构体 refcount

JAVA中写时复制(Copy-On-Write)Map实现

1,什么是写时复制(Copy-On-Write)容器? 写时复制是指:在并发访问的情景下,当需要修改JAVA中Containers的元素时,不直接修改该容器,而是先复制一份副本,在副本上进行修改.修改完成之后,将指向原来容器的引用指向新的容器(副本容器). 2,写时复制带来的影响 ①由于不会修改原始容器,只修改副本容器.因此,可以对原始容器进行并发地读.其次,实现了读操作与写操作的分离,读操作发生在原始容器上,写操作发生在副本容器上. ②数据一致性问题:读操作的线程可能不会立即读取到新修改的数据

Linux进程管理——fork()和写时复制

写时复制技术最初产生于Unix系统,用于实现一种傻瓜式的进程创建:当发出fork(  )系统调用时,内核原样复制父进程的整个地址空间并把复制的那一份分配给子进程.这种行为是非常耗时的,因为它需要: ·      为子进程的页表分配页面 ·      为子进程的页分配页面 ·      初始化子进程的页表 ·      把父进程的页复制到子进程相应的页中 创建一个地址空间的这种方法涉及许多内存访问,消耗许多CPU周期,并且完全破坏了高速缓存中的内容.在大多数情况下,这样做常常是毫无意义的,因为许多

c++ string写时复制

string写时复制:将字符串str1赋值给str2后,除非str1的内容已经被改变,否则str2和str1共享内存.当str1被修改之后,stl才为str2开辟内存空间,并初始化. #include <cstring> #include <string> #include <cstdio> #include <iostream> using namespace std; void fun1() { string s1 = "hello, worl

再谈QVector与QByteArray——Qt的写时复制(copy on write)技术

Qt作为一个优秀的跨平台开源C++框架,如果我们只停留在使用它的基础上而不深挖其实现手法,实在是浪费这个知识宝库了~我们在之前的博文QVector的内存分配策略与再谈QVector与std::vector--使用装饰者让std::vector支持连续赋值中简单聊了聊QVector内存分配和赋值方面的一点东西,今天接着从QVector展开谈谈Qt的写时复制技术.老实说,"隐式共享,引用计数,写时复制"也是老调重弹的话题了,不过也是QTL与STL最大的区别之一,这篇博文不详谈"写

php引用和写时复制

在php变量中已经发现 zval结构体中有refcount__gc(引用个数) 和 is_ref__gc(是否被引用) 例如: <?php $a="hello world"; ?> 此时PHP会创建一个zval容器 因为这个变量不是一个引用 所以这个容器的is_ref__gc为false 并且refcount__gc为1 再看下面的代码 <?php $a="hello world"; $b=$a; ?> 这里由于$b并不是引用$a 所以这里的

php变量的引用计数器和写时复制

众所周知,PHP是不支持指针的,但是如果希望两个变量同时指向同一内存块怎么办呢?为了解决这个问题,PHP内核里使用了引用计数器. 上篇博文介绍了PHP变量在内核中的存储方式了,zval结构中下面两个成员变量用于引用计数器: is_ref BOOL值,标识变量是否是引用集合. refcount 计算指向引用集合的变量个数. 看下面的php代码 <?php $a = "this is a"; ?> 一个zval结构的实体称为zval容器.在php语言层创建一个变量就会相应地在p