php的copy on write,指的是变量发生改变时内存的分离机制,“写”不能简单的理解为赋值,而是必须满足变量的改变。
我们知道php的变量是由 zval实现的,下面是php5系列zval的定义:
typedef union _zvalue_value {
long lval;
double dval;
struct {
char *val;
int len;
} str;
HashTable *ht;
zend_object_value obj;
zend_ast *ast;
} zvalue_value;
struct _zval_struct {
/* Variable information */
zvalue_value value; /* value */
zend_uint refcount__gc;
zend_uchar type; /* active type */
zend_uchar is_ref__gc;
};
zval结构中,refcount是用来记录指向变量容器(zval)的符号个数,之所以有此设计,也是为了更加高效的利用内存。那写时分离到底是如何进行的呢?我们看下面的例子:
$val = 1;
debug_zval_dump($val);echo "
";
$val2 = $val;
debug_zval_dump($val);echo "
";
debug_zval_dump($val2);echo "
";
$val3 = $val;
debug_zval_dump($val);echo "
";
debug_zval_dump($val2);echo "
";
debug_zval_dump($val3);echo "
";
最终结果如下:
long(1) refcount(2)
long(1) refcount(3)
long(1) refcount(3)
long(1) refcount(4)
long(1) refcount(4)
long(1) refcount(4)
通过debug_zval_dump()函数吧变量在zend内部的表示输出出来,如果安装了xdebug,可以使用xdebug_debug_zval()函数达到类似的效果, 我们可以看到,$val
的引用计数在不断增加,实际上$val2 = $val;
,$val3=$val
...在执行的时候并没有发生内存复制,而是这几个变量的引用指向同一块内存地址。
接着看下面的例子:
$val = 1;
debug_zval_dump($val);echo "
";
$val2 = $val;
debug_zval_dump($val);echo "
";
debug_zval_dump($val2);echo "
";
$val3 = $val;
debug_zval_dump($val);echo "
";
debug_zval_dump($val2);echo "
";
debug_zval_dump($val3);echo "
";
$val3 = 2;
debug_zval_dump($val);echo "
";
debug_zval_dump($val2);echo "
";
debug_zval_dump($val3);echo "
";
最终的结果为:
long(1) refcount(2)
long(1) refcount(3)
long(1) refcount(3)
long(1) refcount(4)
long(1) refcount(4)
long(1) refcount(4)
long(1) refcount(3)
long(1) refcount(3)
long(2) refcount(2)
我们可以看到$val3
被重新赋值之后,$val
的引用计数减少了1,$val3
被分离出来独占一块内存,$val
和 $val2``` 继续指向统一内存地址。从这个过程中,我们大体看到分离的过程了。
当与引用结合之后,情况会有所不同
片段A:
$val = 1;
$val2 = $val;
echo "";debug_zval_dump($val);echo "
";
$val3 = $val; //此处不会分离
所有的变量在未改变之前都是使用一个值,所以没有发生分离。后面谁改变就把谁分离出去,仍然不影响语义。
片段B:
$val = 1;
$val2 = &$val;
echo "";debug_zval_dump($val);echo "
";
$val3 = $val; //此处会分离
语义上是 $val2
和 $val
共享一个值,但val3
单独一块内存,如果此处第四行仍然没有分离的话,语义会产生错误。此处第三行与上面第三行的区别在于$val
的zval的is_ref变成了1,这时php并没有启动写时复制机制,而是直接进行复制了。也就是说向一个is_ref=0的变量要值,不会直接复制,在变量改变时才会复制。向一个is_ref=1的变量要值的时候,会直接触发复制。
片段C
function du($arg){}
$val = range(1,10);
$b = $val;
$start = microtime(true);
for($i=0;$i<=100000;$i++) {
du($val);
}
printf("time: %ss\n", microtime(true) - $start);
结果:
time: 0.042001962661743s
片段D
function du($arg){}
$val = range(1,10);
$b = &$val;
$start = microtime(true);
for($i=0;$i<=100000;$i++) {
du($val);
}
printf("time: %ss\n", microtime(true) - $start);
结果:
time: 0.17601108551025s
上面片段D代码之所以比片段C会消耗较更长的时间,就是因为执行d方法调用的时候,所经历的流程和片段B类似(d函数内部接收了参数的传值,并立刻复制了一份);
在php7发布之后,zval采用了新的机制,这个地方的性能问题已经不复存在。关于php7中是如何处理的,我们下回再讲。
参考资料:
http://php.net/manual/zh/language.references.pass.php
http://www.laruence.com/2018/04/08/3170.html