请给我解释一下它是如何工作的。为什么要从变量将值传递到数组,而不是将字面量增加10倍的内存消耗?
PHP 7.1.17
第一个示例:
<?php
ini_set('memory_limit', '1G');
$array = [];
$row = 0;
while ($row < 2000000) {
$array[] = [1];
if ($row % 100000 === 0) {
echo (memory_get_usage(true) / 1000000) . PHP_EOL;
}
$row++;
}
总内存使用量约70MB
第二个示例:
<?php
ini_set('memory_limit', '1G');
$array = [];
$a = 1;
$row = 0;
while ($row < 2000000) {
$array[] = [$a];
if ($row % 100000 === 0) {
echo (memory_get_usage(true) / 1000000) . PHP_EOL;
}
$row++;
}
总内存使用量〜785MB
如果结果数组为一维,则内存消耗也没有差异。
答案 0 :(得分:3)
这里的关键是[1]
尽管是一个复杂的值,但它是一个常数-编译器每次都可以知道它是相同的。
由于当多个变量具有相同的值时,PHP使用“写时复制”系统,因此编译器可以在代码运行之前为数组实际构建“ zval”结构,并且每次添加新变量时都只需增加其引用计数器即可变量或数组值指向它。 (如果它们中的任何一个以后被修改,它们将在修改前被“分离”到新的zval中,因此在那时候仍将进行额外的复制。)
所以(使用42
来突出显示),这是
$bar = [];
$bar[] = [42];
编译为此(用VLD生成的https://3v4l.org输出):
compiled vars: !0 = $bar
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
3 0 E > ASSIGN !0, <array>
4 1 ASSIGN_DIM !0
2 OP_DATA <array>
3 > RETURN 1
请注意,42
甚至没有显示在VLD输出中,它隐含在第二个<array>
中。因此,唯一的内存使用情况是外部数组存储一长串指针,而这些指针恰好都指向同一zval。
另一方面,使用[$a]
之类的变量时,没有保证值将全部相同。可以分析代码并推断出它们是可能的,因此OpCache可能会进行一些优化,但要靠它自己:
$a = 42;
$foo = [];
$foo[] = [$a];
编译为:
compiled vars: !0 = $a, !1 = $foo
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
3 0 E > ASSIGN !0, 42
4 1 ASSIGN !1, <array>
5 2 INIT_ARRAY ~5 !0
3 ASSIGN_DIM !1
4 OP_DATA ~5
5 > RETURN 1
请注意额外的INIT_ARRAY
操作码-这是使用值[$a]
创建的新zval。这是您所有多余内存的去处-每次迭代都会创建一个碰巧具有相同内容的新数组。
在这里需要指出的是,如果$a
本身是一个复杂的值-数组或对象-在每次迭代中都不会复制 ,因为它将具有自己的引用计数器。您每次仍然需要在循环中创建一个新数组,但是这些数组都将包含指向$a
的写时复制指针,而不是其副本。对于整数(在PHP 7中),这种情况不会发生,因为直接存储整数实际上要比将指针存储到其他存储整数的地方便宜。
还有一个值得关注的变化,因为这可能是您可以手动进行的优化:
$a = 42;
$b = [$a];
$foo = [];
$foo[] = $a;
VLD输出:
compiled vars: !0 = $a, !1 = $b, !2 = $foo
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
3 0 E > ASSIGN !0, 42
4 1 INIT_ARRAY ~4 !0
2 ASSIGN !1, ~4
5 3 ASSIGN !2, <array>
6 4 ASSIGN_DIM !2
5 OP_DATA !0
7 6 > RETURN 1
在这里,创建INIT_ARRAY
时有一个$b
操作码,但是当我们将其添加到$foo
时没有。 ASSIGN_DIM
会发现每次重用$b
zval并增加其参考计数器是安全的。我尚未进行测试,但是我相信这将带您回到与常量[1]
相同的内存使用情况。
验证此处是否正在使用写时复制的最后一种方法是使用debug_zval_dump,它显示值的引用计数。确切的数字总是有些偏差,因为将变量传递给函数本身会创建一个或多个引用,但是您可以从相对值中了解一个好主意:
常量数组:
$foo = [];
for($i=0; $i<100; $i++) {
$foo[] = [42];
}
debug_zval_dump($foo[0]);
显示引用计数为102,因为值在100个副本之间共享。
相同但不是恒定的数组:
$a = 42;
$foo = [];
for($i=0; $i<100; $i++) {
$foo[] = [$a];
}
debug_zval_dump($foo[0]);
显示引用计数为2,因为每个值都有自己的zval。
一次构造并明确重用的数组:
$a = 42;
$b = [$a];
$foo = [];
for($i=0; $i<100; $i++) {
$foo[] = $b;
}
debug_zval_dump($foo[0]);
显示引用计数为102,因为值在100个副本之间共享。
内部的复杂值(也可以尝试$a = new stdClass
等)
$a = [1,2,3,4,5];
$foo = [];
for($i=0; $i<100; $i++) {
$foo[] = [$a];
}
debug_zval_dump($foo[0]);
显示引用计数为2,但是内部数组的引用计数为102:每个外部项都有一个单独的数组,但是它们都包含指向创建为$a
的zval的指针。