我们有一段简单的代码:
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.
$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操作码的实现:
关键是这个:
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有什么不同呢?好几件事!
首先,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)