PHP写时拷贝技术

概念

写入拷贝(Copy-on-write,简称COW)是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个调用者(callers)同时要求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此作法主要的优点是如果调用者没有修改该资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。

PHP中的COW

注意:以下代码基于PHP5.6,PHP7之后引用计数机制有变化。

大家都知道,PHP是由C实现的,可是C是强类型语言,PHP怎么做到弱类型语言。一起来看下,PHP变量在C语言底层中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct _zval_struct zval;
typedef unsigned int zend_uint;
typedef unsigned char zend_uchar;

struct _zval_struct {
zvalue_value value; /*注意这里,这个里面存的才是变量的值*/
zend_uint refcount__gc; /*引用计数*/
zend_uchar type; /* 变量当前的数据类型 */
zend_uchar is_ref__gc; /*变量是否引用*/
};

typedef union _zvalue_value {
long lval; /*PHP中整型的值*/
double dval; /*PHP的浮点数值*/
struct {
char *val;
int len;
} str; /*PHP的字符串*/
HashTable *ht; /*数组*/
zend_object_value obj; /*对象*/
} zvalue_value;

PHP的变量,低层是一个结构体zval,里面的zvalue_value结构体实际上是个联合体,这个联合体才是实际存放着PHP的变量值。 Zend引擎为了区别同一个zval地址是否被多个变量共享,引入了ref_countis_ref两个变量进行标识。

运行以下代码,观察变量refcount的变化:

1
2
3
4
5
6
7
8
9
10
11
12
<?php  
$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变量的值的只由\$foo变量指向。当​\$foo的值被赋给​\$bar时,PHP并没有将内存复制一份交给\$bar,而是把\$foo和\$bar指向一个地址, 同时引用计数增加1,也就是新的2。随后,我们更改了\$bar的值,这时如果直接需该\$bar变量指向的内存,则​\$foo的值也会跟着改变。这不是我们想要的结果。于是,PHP内核将内存复制出来一份,并将其值更新为赋值的:2(这个操作也称为变量分离操作),同时原​\$foo变量指向的内存只有$foo指向,所以引用计数更新为:refcount=1。

下面让我们看一个查看内存的例子,可以更容易看到COW在内存使用优化方面的明显作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php  
$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_copy和​\$tipi变量的数据共同指向同一块内存,而没有复制。

也就是说,即使我们不使用引用,一个变量被赋值后,只要我们不改变变量的值 ,也不会新申请内存用来存放数据。据此我们很容易就可以想到一些COW可以非常有效的控制内存使用的场景:只是使用变量进行计算而很少对其进行修改操作,如函数参数的传递,大数组的复制等等等不需要改变变量值的情形。

总结

如果通过赋值的方式赋值给变量时不会申请新内存来存放新变量所保存的值,而是简单的通过一个计数器来共用内存,只有在其中的一个引用指向变量的值发生变化时才申请新空间来保存值内容以减少对内存的占用。

在很多场景下PHP都COW进行内存的优化。比如:变量的多次赋值、函数参数传递,并在函数体内修改实参等。

另外,PHP7的zval容器和引用计数机制发生了改变,可参考文章:

有用就打赏一下作者吧!