PHP内核探索:写时复制COW机制

写时复制(Copy-on-Write,也缩写为COW),顾名思义,就是在写入时才真正复制一份内存进行修改。 COW最早应用在*nix系统中对线程与内存使用的优化,后面广泛的被使用在各种编程语言中,如C++的STL等。 在PHP内核中,COW也是主要的内存优化手段。 在前面关于变量和内存的讨论中,引用计数对变量的销毁与回收中起着至关重要的标识作用。 引用计数存在的意义,就是为了使得COW可以正常运作,从而实现对内存的优化使用。

写时复制的作用

经过上面的描述,大家可能会COW有了个主观的印象,下面让我们看一个小例子, 非常容易看到COW在内存使用优化方面的明显作用:

1$j = 1; 2var_dump(memory_get_usage()); 3$tipi = array_fill(0, 100000, 'php-internal'); 4var_dump(memory_get_usage()); 5 6$tipi_copy = $tipi; 7var_dump(memory_get_usage()); 8 9foreach($tipi_copy as $i){ 10 $j += count($i); 11} 12var_dump(memory_get_usage()); 13 14//-----执行结果----- 15$ php t.php 16int(630904) 17int(10479840) 18int(10479944) 19int(10480040)

上面的代码比较典型的突出了COW的作用,在一个数组变量$tipi被赋值给$tipi_copy时, 内存的使用并没有立刻增加一半,甚至在循环遍历数 $tipi_copy时, 实际上遍历的,仍是$tipi指向的同一块内存。

也就是说,即使我们不使用引用,一个变量被赋值后,只要我们不改变变量的值 ,也与使用引用一样。 进一步讲,就算变量的值立刻被改变,新值的内存分配也会洽如其分。 据此我们很容易就可以想到一些COW可以非常有效的控制内存使用的场景, 如函数参数的传递,大数组的复制等等。

在这个例子中,如果$tipi_copy的值发生了变化,$tipi的值是不应该发生变化的, 那么,此时PHP内核又会如何去做呢?我们引入下面的示例:

1//$tipi = array_fill(0, 3, 'php-internal'); 2//这里不再使用array_fill来填充 ,为什么? 3$tipi[0] = 'php-internal'; 4$tipi[1] = 'php-internal'; 5$tipi[2] = 'php-internal'; 6var_dump(memory_get_usage()); 7 8$copy = $tipi; 9xdebug_debug_zval('tipi', 'copy'); 10var_dump(memory_get_usage()); 11 12$copy[0] = 'php-internal'; 13xdebug_debug_zval('tipi', 'copy'); 14var_dump(memory_get_usage()); 15 16//-----执行结果----- 17$ php t.php 18int(629384) 19tipi: (refcount=2, is_ref=0)=array (0 => (refcount=1, is_ref=0)='php-internal', 1 => (refcount=1, is_ref=0)='php-internal', 2 => (refcount=1, is_ref=0)='php-internal') 20copy: (refcount=2, is_ref=0)=array (0 => (refcount=1, is_ref=0)='php-internal', 1 => (refcount=1, is_ref=0)='php-internal', 2 => (refcount=1, is_ref=0)='php-internal') 21int(629512) 22tipi: (refcount=1, is_ref=0)=array (0 => (refcount=1, is_ref=0)='php-internal', 1 => (refcount=2, is_ref=0)='php-internal', 2 => (refcount=2, is_ref=0)='php-internal') 23copy: (refcount=1, is_ref=0)=array (0 => (refcount=1, is_ref=0)='php-internal', 1 => (refcount=2, is_ref=0)='php-internal', 2 => (refcount=2, is_ref=0)='php-internal') 24int(630088)

从上面例子我们可以看出,当一个数组整个被赋给一个变量时,只是将内存将内存地址赋值给变量。 当数组的值被改变时,Zend内核重新申请了一块内存,然后赋之以新值,但不影响其他值的内存状态。 写时复制的最小粒度,就是zval结构体, 而对于zval结构体组成的集合(如数组和对象等),在需要复制内存时,将复杂对象分解为最小粒度来处理。 这样做就使内存中复杂对象中某一部分做修改时,不必将该对象的所有元素全部“分离”出一份内存拷贝, 从而节省了内存的使用。

写时复制的实现

由于内存块没有办法标识自己被几个指针同时使用, 仅仅通过内存本身并没有办法知道什么时候应该进行复制工作, 这样就需要一个变量来标识这块内存是“被多少个变量名指针同时指向的”, 这个变量,就是前面关于变量的章节提到的:引用计数。

这里有一个比较典型的例子:

1$foo = 1; 2 xdebug_debug_zval('foo'); 3 $bar = $foo; 4 xdebug_debug_zval('foo'); 5 $bar = 2; 6 xdebug_debug_zval('foo'); 7//-----执行结果----- 8foo: (refcount=1, is_ref=0)=1 9foo: (refcount=2, is_ref=0)=1 10foo: (refcount=1, is_ref=0)=1

经过前文对变量的章节,我们可以理解当$foo被赋值时,$foo变量的引用计数为1。 当$foo的值被赋给$bar时,PHP并没有将内存直接复制一份交给$bar, 而是直接把$foo和$bar指向同一个地址。这时,我们可以看到refcount=2; 最后,我们更改了$bar的值,这时如果两个变量再指向同一个内存地址的话, 其值就会同时改变,于是,PHP内核这时将内存复制出来一份,并将其值写为2 ,(这个操作也称为分离操作), 同时维护原$foo变量的引用计数:refcount=1。

上面小例子中的xdebug_debug_zval()是xdebug扩展中的一个函数,用于输出变量在zend内部的引用信息。 如果你没有安装xdebug扩展,也可以使用debug_zval_dump()来代替。 参考:http://www.php.net/manual/zh/function.debug-zval-dump.php

写时复制应用的场景很多,最常见是赋值和函数传参。 在上面的例子中,就使用了zend_assign_to_variable()函数(Zend/zend_execute.c) 对变量的赋值进行了各种判断和处理。 其中最终处理代码如下:

1if (PZVAL_IS_REF(value) && Z_REFCOUNT_P(value) > 0) { 2 ALLOC_ZVAL(variable_ptr); 3 *variable_ptr_ptr = variable_ptr; 4 *variable_ptr = *value; 5 Z_SET_REFCOUNT_P(variable_ptr, 1); 6 zval_copy_ctor(variable_ptr); 7} else { 8 *variable_ptr_ptr = value; 9 Z_ADDREF_P(value); 10}

从这段代码可以看出,如果要进行操作的值已经是引用类型(如已经被&操作符操作过), 则直接重新分配内存,否则只是将value的地址赋与变量,同时将值的zval_value.refcount进行加1操作。

如果大家看过前面的章节, 应该对变量存储的结构体zval(Zend/zend.h)还有印象:

1typedef struct _zval_struct zval; 2... 3struct _zval_struct { 4 /* Variable information */ 5 zvalue_value value; /* value */ 6 zend_uint refcount__gc; 7 zend_uchar type; /* active type */ 8 zend_uchar is_ref__gc; 9};

PHP对值的写时复制的操作,主要依赖于两个参数:refcount__gc与is_ref__gc。 如果是引用类型,则直接进行“分离”操作,即时分配内存, 否则会写时复制,也就是在修改其值的时候才进行内存的重新分配。

写时复制的规则比较繁琐,什么情况会导致写时复制及分离,是有非常多种情况的。 在这里只是举一个简单的例子帮助大家理解,后续会在附录中列举PHP中所有写时复制的相关规则。

写时复制的矛盾,PHP中不推荐使用&操作符的部分解释

上面是一个比较典型的例子,但现实中的PHP实现经过各种权衡, 甚至有时对一个特性的支持与否,是互相矛盾且难以取舍的。 比如,unset()看上去是用来把变量释放,然后把内存标记于空闲的。 可是,在下面的例子中,unset并没有使内存池的内存增加:

1<?php 2$nowamagic = 10; 3$o_o = &$nowamagic; 4unset($o_o); 5echo $nowamagic; 6?>

理论上$o_o是$nowamagic的引用,这两者应该指向同一块内存,其中一个被标识为回收, 另一个也应该被回收才是。但这是不可能的,因为内存本身并不知道都有哪些指针 指向了自已。在C中,o_o这时的值应该是无法预料的, 但PHP不想把这种维护变量引用的工作交给用户,于是, 使用了折中的方法,unset()此时只会把nowamagic变量名从hashtable中去掉, 而内存值的引用计数减1。实际的内存使用完全没有变化。

试想,如果$nowamagic是一个非常大的数组,或者是一个资源型的变量。 这种情形绝对是我们不想看到的。

上面这个例子我们还可以理解,如果每个这种类似操作都要用户来关心。 那PHP就是变换了语法的C了。而下面的这个例子,与其说是语言特性, 倒不如说是更像BUG多一些。(事实上对此在PHP官方的邮件组里有也争论)

1<?php 2$foo ['love'] = 1; 3$bar = &$foo['love']; 4$tipi = $foo; 5$tipi['love'] = '2'; 6echo $foo['love']; 7?>

这个例子最后会输出 2 , 大家会非常惊讶于$nowamagic怎么会影响到$foo, 这完全是两个不同的变量么!至少我们希望是这样。

最后,不推荐大家使用 & ,让PHP自己决定什么时候该使用引用好了, 除非你知道自己在做什么。

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

java 复制Map对象(深拷贝与浅拷贝)

java复制Map对象(深拷贝与浅拷贝)CreationTime2018年6月4日10点00分Author:Marydon1.深拷贝与浅拷贝  浅拷贝:只复制对象的引用,两个引用仍然指向同一个对象

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )