请告诉我它是怎么工作的。为什么要从变量向数组传递值,而不是在10倍的时间内增加内存消耗?
php浏览器
第一个例子:
<?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
如果得到的数组是一维的,则内存消耗也没有差别。
最佳答案
这里的关键是,[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
注意额外的操作码-这是一个新的zval,其值为
INIT_ARRAY
。这是所有额外内存的地方-每次迭代都会创建一个新数组,恰好有相同的内容。这里要指出的是,如果cc本身是一个复杂的值——数组或对象——它不会在每次迭代中被复制,因为它将有自己的引用计数器。在循环中每次都会创建一个新数组,但是这些数组都包含指向
[$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
这里,我们在创建
$a
时有一个$a
操作码,但在将其添加到INIT_ARRAY
时没有。$b
将看到每次重用$foo
zval是安全的,并增加其参考计数器。我还没有测试过,但我相信这会让你回到常量ASSIGN_DIM
情况下的内存使用情况。最后一种验证是否正在使用写时复制的方法是使用debug_zval_dump,它显示值的引用计数。确切的数字总是有点偏离,因为把变量传递给函数本身就创建了一个或多个引用,但是你可以从相对值中得到一个好的想法:
常数数组:
$foo = [];
for($i=0; $i<100; $i++) {
$foo[] = [42];
}
debug_zval_dump($foo[0]);
显示refcount为102,因为值在100个副本之间共享。
相同但不是常数数组:
$a = 42;
$foo = [];
for($i=0; $i<100; $i++) {
$foo[] = [$a];
}
debug_zval_dump($foo[0]);
显示refcount为2,因为每个值都有自己的zval。
数组构造一次并显式重用:
$a = 42;
$b = [$a];
$foo = [];
for($i=0; $i<100; $i++) {
$foo[] = $b;
}
debug_zval_dump($foo[0]);
显示refcount为102,因为值在100个副本之间共享。
内部复杂值(也可尝试
$b
等):$a = [1,2,3,4,5];
$foo = [];
for($i=0; $i<100; $i++) {
$foo[] = [$a];
}
debug_zval_dump($foo[0]);
显示refcount为2,但内部数组的refcount为102:每个外部项都有一个单独的数组,但它们都包含指向创建为
[1]
的zval的指针。