PHP:写入时复制和通过引用分配在 PHP5 和 PHP7 上执行不同

Posted

技术标签:

【中文标题】PHP:写入时复制和通过引用分配在 PHP5 和 PHP7 上执行不同【英文标题】:PHP: Copy On Write and Assign By Reference perform different on PHP5 and PHP7 【发布时间】:2016-07-20 05:12:59 【问题描述】:

我们有一段简单的代码:

1    <?php
2    $i = 2;
3    $j = &$i;
4    echo (++$i) + (++$i);

在 PHP5 上,它输出 8,因为:

$i 是一个引用,当我们将$i 增加++i 时,它会更改zval 而不是复制,所以第4 行将是4 + 4 = 8。这是通过引用分配

如果我们注释第 3 行,它将输出 7,每次我们通过增加它来更改值,PHP 都会复制,第 4 行将是3 + 4 = 7。这是写入时复制

但在 PHP7 中,它总是输出 7。

我检查了 PHP7 中的更改:http://php.net/manual/en/migration70.incompatible.php,但我没有得到任何线索。

任何帮助都会很棒,在此先感谢。

更新1

这是 PHP5 / PHP7 上的代码结果:https://3v4l.org/USTHR

更新2

操作码:

[huqiu@101 tmp]$ php -d vld.active=1 -d vld.execute=0 -f incr-ref-add.php
Finding entry points
Branch analysis from position: 0
Jump found. Position 1 = -2
filename:       /home/huqiu/tmp/incr-ref-add.php
function name:  (null)
number of ops:  7
compiled vars:  !0 = $i, !1 = $j
line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
   2     0  E >   ASSIGN                                                   !0, 2
   3     1        ASSIGN_REF                                               !1, !0
   4     2        PRE_INC                                          $2      !0
         3        PRE_INC                                          $3      !0
         4        ADD                                              ~4      $2, $3
         5        ECHO                                                     ~4
   5     6      > RETURN                                                   1

branch: #  0; line:     2-    5; sop:     0; eop:     6; out1:  -2
path #1: 0,

【问题讨论】:

这闻起来像以前版本的 PHP 中的错误。 通过阅读文档,我没有看到保证任何评估顺序的明确声明,因此代码依赖于未指定的实现细节。说明:++$i 将首先递增$i,然后返回$i,根据文档。从变量$i 到整数值(加号操作所必需的)的转换是发生在第二次递增操作之前还是之后没有定义。 嗨,@UlrichEckhardt 我更新了问题并添加了操作码,似乎增量在ADDECHO 之前。 我已经给php internals发了一封邮件,希望那里的人能给我一些线索。 我手头没有链接,但我清楚地记得 PHP 文档中声明无法保证评估顺序。 【参考方案1】:

免责声明:我不是 PHP 内部专家(还没有?)所以这都是我的理解,不能保证 100% 正确或完整。 :)

因此,首先,PHP 7 的行为——我注意到,HHVM 也遵循该行为——似乎是正确的,而 PHP 5 在这里有一个错误。这里不应该有额外的引用行为,因为无论执行顺序如何,对++$i 的两次调用的结果都不应该相同。

操作码看起来不错;至关重要的是,我们有两个临时变量 $2$3,用于保存两个增量结果。但不知何故,PHP 5 表现得好像我们写过这样的:

$i = 2;
$i++; $temp1 =& $i;
$i++; $temp2 =& $i;
echo $temp1 + $temp2; 

而不是这样:

$i = 2;
$i++; $temp1 = $i;
$i++; $temp2 = $i;
echo $temp1 + $temp2; 

编辑: PHP Internals 邮件列表中指出,在单个语句中使用多个修改变量的操作通常被认为是“未定义行为”,++ 是 used as an example of this in C/C++ .

因此,PHP 5 出于实现/优化的原因返回它所做的值是合理的,即使它在逻辑上与合理的序列化为多个语句不一致。

(相对较新的)PHP language specification 包含类似的语言和示例:

除非在本规范中明确说明,否则表达式中的操作数相对于彼此计算的顺序是未指定的。 [...](例如,完整表达式$j = $i + $i++中的[...],$i的值是旧$i还是新$i,未指定。)

可以说,这是一个比“未定义行为”更弱的说法,因为这意味着它们是以某种特定的顺序进行评估的,但我们现在开始吹毛求疵了。

phpdbg 调查(PHP 5)

我很好奇,想了解更多关于内部的信息,所以使用phpdbg进行了一些尝试。

没有参考

使用$j = $i 代替$j =&amp; $i 运行代码,我们从共享地址的2 个变量开始,引用计数为2(但没有is_ref 标志):

Address         Refs    Type            Variable
0x7f3272a83be8  2       (integer)       $i
0x7f3272a83be8  2       (integer)       $j

但是一旦你预先增加,zval 就会被分开,只有一个 temp var 与 $i 共享,引用计数为 2:

Address         Refs    Type            Variable
0x7f189f9ecfc8  2       (integer)       $i
0x7f189f859be8  1       (integer)       $j

有参考赋值

当变量绑定在一起时,它们共享一个地址,引用计数为 2,并带有一个 by-ref 标记:

Address         Refs    Type            Variable
0x7f9e04ee7fd0  2       (integer)       &$i
0x7f9e04ee7fd0  2       (integer)       &$j

在预增量之后(但在加法之前),相同地址的引用计数为 4,显示 2 个临时变量错误地被引用绑定:

Address         Refs    Type            Variable
0x7f9e04ee7fd0  4       (integer)       &$i
0x7f9e04ee7fd0  4       (integer)       &$j

问题的根源

深入http://lxr.php.net上的源码,我们可以找到ZEND_PRE_INC操作码的实现:

PHP 5.6 PHP 7.0

PHP 5

关键的一行是这样的:

 SEPARATE_ZVAL_IF_NOT_REF(var_ptr);

所以我们为结果值创建一个新的 zval只有当它当前不是一个引用时。再往下,我们有这个:

if (RETURN_VALUE_USED(opline)) 
    PZVAL_LOCK(*var_ptr);
    EX_T(opline->result.var).var.ptr = *var_ptr;

所以如果实际使用了减量的返回值,我们需要先“锁定”zval,这在一系列宏之后基本上意味着“增加它的引用计数”,然后再将其分配为结果。

如果我们之前创建了一个新的 zval,那很好 - 我们的 refcount 现在是 2,实际变量为 1,运算结果为 1。但是如果我们决定不这样做,因为我们需要保存一个引用,我们只是增加现有的引用计数,并指向一个可能即将再次更改的 zval。

PHP 7

那么 PHP 7 有什么不同呢?几件事!

首先,phpdbg 的输出相当无聊,因为在 PHP 7 中整数不再被引用计数;相反,引用赋值会创建一个额外的指针,它本身的引用计数为 1,指向内存中的同一地址,即实际的整数。 phpdbg 输出如下所示:

Address            Refs    Type      Variable
0x7f175ca660e8     1       integer   &$i
int (2)
0x7f175ca660e8     1       integer   &$j
int (2)

其次,the source 中有一个特殊的整数代码路径:

if (EXPECTED(Z_TYPE_P(var_ptr) == IS_LONG)) 
    fast_long_increment_function(var_ptr);
    if (UNEXPECTED(RETURN_VALUE_USED(opline))) 
        ZVAL_COPY_VALUE(EX_VAR(opline->result.var), var_ptr);
    
    ZEND_VM_NEXT_OPCODE();

因此,如果变量是整数 (IS_LONG) 而不是对整数的引用 (IS_REFERENCE),那么我们可以就地递增它。如果我们需要返回值,我们可以将它的值复制到结果中(ZVAL_COPY_VALUE)。

如果它是一个引用,我们将不会点击该代码,而是将引用绑定在一起,我们有以下两行:

ZVAL_DEREF(var_ptr);
SEPARATE_ZVAL_NOREF(var_ptr);

第一行说“如果它是一个参考,就跟随它到它的目标”;这将我们从“对整数的引用”带到了整数本身。第二个 - 我认为 - 说“如果它是被引用的东西,并且有多个引用,请创建它的副本”;在我们的例子中,这不会做任何事情,因为整数不关心引用计数。

所以现在我们有了一个可以递减的整数,它会影响所有的按引用关联,但不会影响引用计数类型的按值关联。最后,如果我们想要增量的返回值,我们再次复制它,而不是仅仅分配它;这次使用了一个稍微不同的宏,如果需要的话,它会增加我们新 zval 的引用计数:

ZVAL_COPY(EX_VAR(opline->result.var), var_ptr);

【讨论】:

【参考方案2】:

我想说它在 PHP7 中的工作方式是正确的。根据是否在任何地方引用操作数来隐式更改运算符的工作方式是不好的。

这是完全重写 PHP7 的最大好处:没有笨拙/错误驱动的开发 v4/v5 代码将起作用。

【讨论】:

以上是关于PHP:写入时复制和通过引用分配在 PHP5 和 PHP7 上执行不同的主要内容,如果未能解决你的问题,请参考以下文章

php数组通过复制值或引用分配? [复制]

php 变量的分配和销毁

PHP 内存管理 写时复制 垃圾回收

PHP7为什么比PHP5快?

关于PHP5与PHP7的若干问题

在 PHP 5 中如何通过引用传递对象?