PHP 7.4 Zvals的引用计数

Details of PHP 7.4 Reference Counting of Zvals

根据阅读此处描述的 zvals 的内部实现,我有一些澄清点 Internal value representation in PHP 7 - Part 1 and Internal value representation in PHP 7 - Part 2

在详细解释我的困惑之前,我认为可以总结为:

(i) 我不明白为什么对象引用计数不同于数组和字符串

(ii) 为什么字符串的引用计数不同于数组。

据我了解,对于字符串、数组和对象等“复杂”数据类型,这些实体是引用计数类型(在 PHP 7.4 中)。

1 因此,基于这种简单化的观点,我认为 PHP 中的数组、字符串和对象的引用计数是相同的,但事实并非如此。有人可以解释一下这种(显然)过于简单化的观点在哪里会崩溃吗?

我使用了'debug_zval_dump'函数来计算引用(我没有权限安装xdebug来使用'xdebug_debug_zval')。在我随后的分析中,我假设函数自身的参数会影响显示的引用计数。

2 对于对象,以下计数对我来说很有意义,但如果能得到确认就太好了。

class Foo{}
$a = new Foo();
debug_zval_dump($a); // #1: $a refcount = 2 -- and assume this is run after each line of code for each variable
$b = $a; // #2: $a, $b refcount = 3
$c = &$a; // #3: $a, $b, and $c refcount = 3
$a = new Foo(); // #4 $a, $b, and $c refcount = 2

明确一点:每次定义或更新变量时,我都会将所有现有变量传递给 PHP 7.4 引擎中的 debug_zval_dump。这就是 refcounts 所指的。我只是在保存 space.

#1:有两个_zval_struct指向对象(一个是函数参数)

#2: 有3个_zval_struct指向对象(算上函数参数)

#3:函数参数,$b,azend_reference($a和$c共享)指向对象

#4:$a和$c通过commonzend_reference(加上函数参数)引用同一个对象,$b和函数参数引用同一个对象。

这个算错了吗?如果是这样,请纠正我。否则,我们转到更混乱的项目:

3 个数组:

$a=[]; // #1: $a refcount = 1
$b=$a; // #2: $a, $b refcount = 1
$c=&$a; // #3: $a, $b, and $c refcount = 1
$a[]=0; // #4: $a and $c refcount = 2, $b refcount = 1 

我希望得到与 2.#1-#4 相同的数字,但这不是我们得到的。这似乎与链接到 as 的 PHP 文章不一致,我希望在 #4:

$a, $c -> zend_reference1(refcount=2) -> zend_array2(refcount=2,value=[0])

$参数----------------------------^

$b, $parameter -> zend_array1(refcount=2,value=[])

4 然后还有不同的字符串计数。

$a=''; // #1: $a refcount = 1
$b=$a; // #2: $a, $b refcount = 1
$c=&$a; // #3: $a, $b, and $c refcount = 1
$a='foo'; // #4: $a, $b, and $c refcount = 1

我这里的图表和 3 一样。

我忽略了这个引用计数的哪些细节?

5 作为奖励,现在引用一个数字会发生什么?例如

$a=0;
$b=&$a; // $a, b -> zend_reference(refcount=2) -> zend_value(value=0)

注释图是否正确,假设 zend_value 是基于堆栈的(因为数值不是引用计数)?

So based on this simplistic view, I would imagine the reference counting for arrays, strings, and objects in PHP are the same, but it appears not to be the case. Can someone explain where this (apparently) overly simplistic view falls apart?

它分崩离析是因为变量的表示方式不仅基于它们的 类型,还基于它们的定义和使用方式 .特别是,copy-on-write 并不总是处理简单值的最有效方式,编译器可以执行其他优化。

这在 debug_zval_dump 中很难看出,因为将变量传递给函数会改变它的表示形式,并且因为有一些细节它没有显示。相反,使用 the xdebug_debug_zval function provided by Xdebug,我们可以获得更多信息...

对于对象情况,相当 straight-forward:

class Foo{}
$a = new Foo();
// a: (refcount=1, is_ref=0)=class Foo {  }
$b = $a;
// a: (refcount=2, is_ref=0)=class Foo {  }                                                                                                                                                                         // b: (refcount=2, is_ref=0)=class Foo {  }
$c = &$a;
// a: (refcount=2, is_ref=1)=class Foo {  }
// b: (refcount=2, is_ref=0)=class Foo {  }
// c: (refcount=2, is_ref=1)=class Foo {  }
$a = new Foo();
// a: (refcount=2, is_ref=1)=class Foo {  }
// b: (refcount=1, is_ref=0)=class Foo {  }
// c: (refcount=2, is_ref=1)=class Foo {  }

$a$c 指向同一个 IS_REFERENCE zval (refcount=2); zval 和 $b 都指向同一个对象 (refcount=2).


现在让我们看看数组的情况:

$a=[];
// a: (immutable, is_ref=0)=array ()
$b=$a;
// a: (immutable, is_ref=0)=array ()
// b: (immutable, is_ref=0)=array ()
$c=&$a;
// a: (refcount=2, is_ref=1)=array ()
// b: (immutable, is_ref=0)=array ()
// c: (refcount=2, is_ref=1)=array ()
$a[]=0;
// a: (refcount=2, is_ref=1)=array (0 => (refcount=0, is_ref=0)=0)
// b: (immutable, is_ref=0)=array ()
// c: (refcount=2, is_ref=1)=array (0 => (refcount=0, is_ref=0)=0)

空数组根本不显示引用计数,它们显示“不可变”。空数组很常见,并且可以互换,因此一种特殊情况可以避免分配大量具有相同内容的单独 zval。

如果我们将数组改为非空数组,我们会得到不同的结果:

$a=[42];
// a: (refcount=2, is_ref=0)=array (0 => (refcount=0, is_ref=0)=42)
$b=$a;
// a: (refcount=3, is_ref=0)=array (0 => (refcount=0, is_ref=0)=42)
// b: (refcount=3, is_ref=0)=array (0 => (refcount=0, is_ref=0)=42)
$c=&$a;
// a: (refcount=2, is_ref=1)=array (0 => (refcount=0, is_ref=0)=42)
// b: (refcount=3, is_ref=0)=array (0 => (refcount=0, is_ref=0)=42)
// c: (refcount=2, is_ref=1)=array (0 => (refcount=0, is_ref=0)=42)
$a[]=0;
// a: (refcount=2, is_ref=1)=array (0 => (refcount=0, is_ref=0)=42, 1 => (refcount=0, is_ref=0)=0)
// b: (refcount=2, is_ref=0)=array (0 => (refcount=0, is_ref=0)=42)
// c: (refcount=2, is_ref=1)=array (0 => (refcount=0, is_ref=0)=42, 1 => (refcount=0, is_ref=0)=0)

“不可变”消失了,但有些奇怪:即使我们只分配一个变量,引用计数也从 2 开始。这是一个“编译变量”的影响:编译器有 pre-allocated 包含 [42] 的 zval,因此需要为其管理内存。为了避免正常的内存管理过早释放它,它添加了对 zval 的额外计数引用。

为了打败那个优化,让我们创建一个只能在run-time:

处创建的数组
$a=[rand()];
// a: (refcount=1, is_ref=0)=array (0 => (refcount=0, is_ref=0)=713417292)
$b=$a;
// a: (refcount=2, is_ref=0)=array (0 => (refcount=0, is_ref=0)=713417292)
// b: (refcount=2, is_ref=0)=array (0 => (refcount=0, is_ref=0)=713417292)
$c=&$a;
// a: (refcount=2, is_ref=1)=array (0 => (refcount=0, is_ref=0)=713417292)
// b: (refcount=2, is_ref=0)=array (0 => (refcount=0, is_ref=0)=713417292)
// c: (refcount=2, is_ref=1)=array (0 => (refcount=0, is_ref=0)=713417292)
$a[]=0;
// a: (refcount=2, is_ref=1)=array (0 => (refcount=0, is_ref=0)=713417292, 1 => (refcount=0, is_ref=0)=0)
// b: (refcount=1, is_ref=0)=array (0 => (refcount=0, is_ref=0)=713417292)
// c: (refcount=2, is_ref=1)=array (0 => (refcount=0, is_ref=0)=713417292, 1 => (refcount=0, is_ref=0)=0)

最后,事情看起来更像对象案例了!


关于字符串...

$a='';
// a: (interned, is_ref=0)=''
$b=$a;
// a: (interned, is_ref=0)=''
// b: (interned, is_ref=0)=''
$c=&$a;
// a: (refcount=2, is_ref=1)=''
// b: (interned, is_ref=0)=''
// c: (refcount=2, is_ref=1)=''
$a='foo';
// a: (refcount=2, is_ref=1)='foo'
// b: (interned, is_ref=0)=''
// c: (refcount=2, is_ref=1)='foo'

与空数组一样,空字符串不显示引用计数,而是显示“interned”。同样,编译器决定不分配新的 zval 而是使用一些共享内存。 ('foo' 可能也被 interned,但 Xdebug 向我们展示了指向它的 IS_REFERENCE zval。)

让我们选择一些东西 non-empty 代替:

$a='hello';
// a: (interned, is_ref=0)='hello'
$b=$a;
// a: (interned, is_ref=0)='hello'
// b: (interned, is_ref=0)='hello'
$c=&$a;
// a: (refcount=2, is_ref=1)='hello'
// b: (interned, is_ref=0)='hello'
// c: (refcount=2, is_ref=1)='hello'
$a='foo';
// a: (refcount=2, is_ref=1)='foo'
// b: (interned, is_ref=0)='hello'
// c: (refcount=2, is_ref=1)='foo'

与 non-empty 数组不同,这没有任何区别,编译器可以“实习”它在源代码中看到的任何常量字符串。

所以我们需要再次打败优化:

$a=(string)rand();
// a: (refcount=1, is_ref=0)='522057011'
$b=$a;
// a: (refcount=2, is_ref=0)='522057011'
// b: (refcount=2, is_ref=0)='522057011'
$c=&$a;
// a: (refcount=2, is_ref=1)='522057011'
// b: (refcount=2, is_ref=0)='522057011'
// c: (refcount=2, is_ref=1)='522057011'
$a='foo';
// a: (refcount=2, is_ref=1)='foo'
// b: (refcount=1, is_ref=0)='522057011'
// c: (refcount=2, is_ref=1)='foo'

再次匹配对象示例!


这只是当前引擎中涉及的优化的一个示例,未来的版本中可能会添加更多(以上是在 PHP 7.4、8.0 和 8.1 上测试的)。