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

Wesley13
• 阅读 366

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

写时复制的作用

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

$j = 1;  
var_dump(memory_get_usage());  
$tipi = array_fill(0, 100000, 'php-internal');  
var_dump(memory_get_usage());  
    
$tipi_copy = $tipi;  
var_dump(memory_get_usage());  
    
foreach($tipi_copy as $i){  
    $j += count($i);  
}  
var_dump(memory_get_usage());  
    
//-----执行结果-----  
$ php t.php  
int(630904)  
int(10479840)  
int(10479944)  
int(10480040)

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

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

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

//$tipi = array_fill(0, 3, 'php-internal');   
//这里不再使用array_fill来填充 ,为什么?  
$tipi[0] = 'php-internal';  
$tipi[1] = 'php-internal';  
$tipi[2] = 'php-internal';  
var_dump(memory_get_usage());  
    
$copy = $tipi;  
xdebug_debug_zval('tipi', 'copy');  
var_dump(memory_get_usage());  
    
$copy[0] = 'php-internal';  
xdebug_debug_zval('tipi', 'copy');  
var_dump(memory_get_usage());  
    
//-----执行结果-----  
$ php t.php  
int(629384)  
tipi: (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')  
copy: (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')  
int(629512)  
tipi: (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')  
copy: (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')  
int(630088)  

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

写时复制的实现

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

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

$foo = 1;  
 xdebug_debug_zval('foo');  
 $bar = $foo;  
 xdebug_debug_zval('foo');  
 $bar = 2;  
 xdebug_debug_zval('foo');    
//-----执行结果-----  
foo: (refcount=1, is_ref=0)=1  
foo: (refcount=2, is_ref=0)=1  
foo: (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) 对变量的赋值进行了各种判断和处理。 其中最终处理代码如下:

if (PZVAL_IS_REF(value) && Z_REFCOUNT_P(value) > 0) {  
    ALLOC_ZVAL(variable_ptr);  
    *variable_ptr_ptr = variable_ptr;  
    *variable_ptr = *value;  
    Z_SET_REFCOUNT_P(variable_ptr, 1);  
    zval_copy_ctor(variable_ptr);  
} else {  
    *variable_ptr_ptr = value;  
    Z_ADDREF_P(value);  
}  

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

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

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

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

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

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

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

<?php  
$nowamagic = 10;  
$o_o  = &$nowamagic;  
unset($o_o);  
echo $nowamagic;  
?>  

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

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

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

<?php  
$foo ['love'] = 1;  
$bar  = &$foo['love'];  
$tipi = $foo;  
$tipi['love'] = '2';  
echo $foo['love'];  
?>  

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

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

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
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
Wesley13 Wesley13
2年前
java 复制Map对象(深拷贝与浅拷贝)
java复制Map对象(深拷贝与浅拷贝)CreationTime2018年6月4日10点00分Author:Marydon1.深拷贝与浅拷贝  浅拷贝:只复制对象的引用,两个引用仍然指向同一个对象
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Wesley13 Wesley13
2年前
Java CopyOnWrite容器
   CopyOnWrite简称COW(写时复制),是一种程序设计中的优化策略,读取时,直接读取,写入时,copy一个副本,在这个副本上进行写入,写入完成,用副本替换原数据,这是一种延时懒惰策略。   从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,CopyOnWriteArrayList和CopyOnW
Stella981 Stella981
2年前
Android So动态加载 优雅实现与原理分析
背景:漫品Android客户端集成适配转换功能(基于目标识别(So库35M)和人脸识别库(5M)),导致apk体积50M左右,为优化客户端体验,决定实现So文件动态加载.!(https://oscimg.oschina.net/oscnet/00d1ff90e4b34869664fef59e3ec3fdd20b.png)点击上方“蓝字”关注我
Wesley13 Wesley13
2年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
PHP垃圾回收机制
php5.3之前使用的垃圾回收机制是单纯的“引用计数”,也就是每个内存对象都分配一个计数器,当内存对象被变量引用时,计数器1;当变量引用撤掉后,计数器1;当计数器0时,表明内存对象没有被使用,该内存对象则进行销毁,垃圾回收完成。“引用计数”存在问题,就是当两个或多个对象互相引用形成环状后,内存对象的计数器则不会消减为0;这时候,这一组内存对象已经
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这