PHP:在写入时复制和按引用分配在PHP5和PHP7上执行不同的操作

sra*_*ain 6 php php-internals

我们有一段简单的代码:

1    <?php
2    $i = 2;
3    $j = &$i;
4    echo (++$i) + (++$i);
Run Code Online (Sandbox Code Playgroud)

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

$i为参考,当我们增加$i++i,它会改变zval,而不是做一个副本,因此4号线会4 + 4 = 8.这是按参考分配.

如果我们评论第3行,它将输出7,每次我们通过增加它来改变它,PHP将复制,第4行将是3 + 4 = 7.这是Copy On Write.

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

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

任何帮助都会很棒,提前谢谢.

UPDATE1

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

UPDATE2

操作码:

[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,
Run Code Online (Sandbox Code Playgroud)

IMS*_*SoP 11

免责声明:我不是PHP Internals专家(但是?)所以这完全取决于我的理解,并不保证100%正确或完整.:)

所以,首先,PHP 7的行为 - 我注意到,HHVM后面也是如此 - 似乎是正确的,PHP 5在这里有一个错误.这里不应该通过引用行为进行额外的赋值,因为无论执行顺序如何,两次调用的结果++$i都不应该相同.

操作码看起来很好; 重要的是,我们有两个临时变量$2$3,以保持两个增量的结果.但不知何故,PHP 5就像我们写的那样:

$i = 2;
$i++; $temp1 =& $i;
$i++; $temp2 =& $i;
echo $temp1 + $temp2; 
Run Code Online (Sandbox Code Playgroud)

而不是这个:

$i = 2;
$i++; $temp1 = $i;
$i++; $temp2 = $i;
echo $temp1 + $temp2; 
Run Code Online (Sandbox Code Playgroud)

编辑:有人指出,PHP内部邮件列表上使用修改单个语句中的变量多个操作通常被认为是"不确定的行为",并且++用作C/C++这样的一个例子.

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

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

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

可以说,这是一个比"未定义的行为"更弱的主张,因为它暗示它们是以某种特定的顺序进行评估,但我们现在正在进行挑选.

phpdbg调查(PHP 5)

我很好奇,想要了解更多关于内部的知识,所以一些人在使用phpdbg.

没有参考

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

Address         Refs    Type            Variable
0x7f3272a83be8  2       (integer)       $i
0x7f3272a83be8  2       (integer)       $j
Run Code Online (Sandbox Code Playgroud)

但是一旦你预先递增,zvals就会被分开,只有一个temp var与$ i共享,给出一个refcount为2:

Address         Refs    Type            Variable
0x7f189f9ecfc8  2       (integer)       $i
0x7f189f859be8  1       (integer)       $j
Run Code Online (Sandbox Code Playgroud)

有参考作业

当变量绑定在一起时,它们共享一个refcount为2的地址和一个by-ref标记:

Address         Refs    Type            Variable
0x7f9e04ee7fd0  2       (integer)       &$i
0x7f9e04ee7fd0  2       (integer)       &$j
Run Code Online (Sandbox Code Playgroud)

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

Address         Refs    Type            Variable
0x7f9e04ee7fd0  4       (integer)       &$i
0x7f9e04ee7fd0  4       (integer)       &$j
Run Code Online (Sandbox Code Playgroud)

问题的根源

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

PHP 5

关键是这个:

 SEPARATE_ZVAL_IF_NOT_REF(var_ptr);
Run Code Online (Sandbox Code Playgroud)

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

if (RETURN_VALUE_USED(opline)) {
    PZVAL_LOCK(*var_ptr);
    EX_T(opline->result.var).var.ptr = *var_ptr;
}
Run Code Online (Sandbox Code Playgroud)

因此,如果实际使用了减量的返回值,我们需要"锁定"zval,它遵循一系列宏基本上意味着"递增其引用计数",然后将其作为结果赋值.

如果我们之前创建了一个新的zval,那很好 - 我们的引用现在为实际变量的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)
Run Code Online (Sandbox Code Playgroud)

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

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();
}
Run Code Online (Sandbox Code Playgroud)

因此,如果变量是一个整数(IS_LONG),而不是一个整数的引用(IS_REFERENCE),那么我们就可以增加它在的地方.如果我们需要返回值,我们可以将其值复制到result(ZVAL_COPY_VALUE)中.

如果它是一个引用,我们将不会命中该代码,而是保持引用绑定在一起,我们有这两行:

ZVAL_DEREF(var_ptr);
SEPARATE_ZVAL_NOREF(var_ptr);
Run Code Online (Sandbox Code Playgroud)

第一行说"如果它是一个参考,请遵循它的目标"; 这将我们从"引用整数"转换为整数本身.第二个 - 我认为 - 说"如果它是重新计算的东西,并且有多个引用,则创建它的副本"; 在我们的例子中,这将不执行任何操作,因为整数不关心refcounts.

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

ZVAL_COPY(EX_VAR(opline->result.var), var_ptr);
Run Code Online (Sandbox Code Playgroud)