学习PHP写时复制
什么叫写时复制(Copy on Write,缩写为COW),这个概念一开始听到时是在操作系统课程上的。PHP中的COW可以简单描述为:如果通过赋值的方式赋值给变量时不会申请新内存来存放新变量所保存的值,而是简单的通过一个计数器来共用内存,只有在其中的一个引用指向变量的值发生变化时才申请新空间来保存值内容以减少对内存的占用。在很多场景下PHP都COW进行内存的优化。比如:变量的多次赋值、函数参数传递,并在函数体内修改实参等。下面用一个实例来证明写时复制对内存开销的节省。
1 2 3 4 5 6 7 8 9 10 11 12 13 | <span style="font-size: medium;">//第一个例子 var_dump(memory_get_usage()); $fill = array_fill(0,100000,'php-nternal'); var_dump(memory_get_usage()); $fill_copy = $fill; var_dump(memory_get_usage()); //输出结果 [root@AY140101000322928a0bZ tmp]# php mem.php int(223424) int(10072376) int(10072480)</span> |
可以看到,对$fill数组进行复制时,内存占用并未大幅度飙升,也就是说,在这种情况下对$fill_copy赋值,只是在符号表中增加多了一个key-value记录,value所指向的zval与$fill指向的zval一致,当指向同一内存的值发生了变化(或者可能发生变化),就需要将变化的值“分离”出去,这个“分离”的操作,就是“复制”。关于变量zval可以参考这里–深入理解PHP变量。
ok,上面尝试的情况是针对is_ref为0,即引用关闭的情况下进行的测试,下面例子对引用开启的情况进行测试,看看php对于这种条件的赋值是怎么处理的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <span style="font-size: medium;">//第二个例子 var_dump(memory_get_usage()); $fill = array_fill(0,100000,'php-nternal'); var_dump(memory_get_usage()); foreach($fill as &$v){ $ref[] = &$v; } var_dump(memory_get_usage()); //输出 [root@AY140101000322928a0bZ tmp]# php mem.php int(223008) int(10071944) int(27921624)</span> |
这里$ref数组的赋值特意取的对$fill每个元素的引用,注意,在for循环中,首先对$v取&,相当于$fill数组每个元素is_ref设置为1,ref_count设置为2,再$ref[] = &$v时ref_count设置为3,当循环结束时,自动注销$v,使得$ref_count恢复2,这便实现is_ref为1时的复制。调整一下,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <span style="font-size: medium;">//第三个例子 var_dump(memory_get_usage()); $fill = array_fill(0,100000,'php-nternal'); var_dump(memory_get_usage()); foreach($fill as &$v){ $ref[] = $v; } var_dump(memory_get_usage()); //输出 [root@AY140101000322928a0bZ tmp]# php mem.php int(223008) int(10071944) int(35922704)</span> |
这样的结果显而易见了,当is_ref=1时,直接进行复制php是会重新分配一个新的zval的。然而上面第二个例子中,为何在for中内存也会大幅度上升?原因是我们不同于之前复制整个数组,而是对数组每个元素进行复制,可见相当于符号表新增了100000个key-value。