PHP:写时复制和引用赋值在PHP5和PHP7上执行不同的操作
我们有一段简单的代码:PHP:写时复制和引用赋值在PHP5和PHP7上执行不同的操作,php,php-internals,Php,Php Internals,我们有一段简单的代码: 1 <?php 2 $i = 2; 3 $j = &$i; 4 echo (++$i) + (++$i); 我认为它在PHP7中的工作方式是正确的。根据操作数是否在任何地方引用,隐式更改运算符的工作方式是不好的 这是完全重写PHP7最好的地方:没有笨拙/错误驱动的开发v4/v5代码可以工作。我认为它在PHP7中的工作方式是正确的。根据操作数是否在任何地方引用,隐式更改运算符的工作方式是不好的 这是完全重写PHP7最好的地方:没有笨拙
1 <?php
2 $i = 2;
3 $j = &$i;
4 echo (++$i) + (++$i);
我认为它在PHP7中的工作方式是正确的。根据操作数是否在任何地方引用,隐式更改运算符的工作方式是不好的
这是完全重写PHP7最好的地方:没有笨拙/错误驱动的开发v4/v5代码可以工作。我认为它在PHP7中的工作方式是正确的。根据操作数是否在任何地方引用,隐式更改运算符的工作方式是不好的
这是完全重写PHP7最好的地方:没有笨拙/错误驱动的开发v4/v5代码可以工作。免责声明:我不是PHP内部专家(还?),所以这都是我的理解,不能保证100%正确或完整。:) 因此,首先,PHP7的行为——我注意到,HHVM也跟着它——似乎是正确的,PHP5在这里有一个bug。这里不应该有额外的按引用赋值行为,因为无论执行顺序如何,对
+$i
的两个调用的结果都不应该相同
操作码看起来不错;至关重要的是,我们有两个临时变量$2
和$3
,用于保存两个增量结果。但不知何故,PHP5表现得就像我们写的那样:
$i = 2;
$i++; $temp1 =& $i;
$i++; $temp2 =& $i;
echo $temp1 + $temp2;
与此相反:
$i = 2;
$i++; $temp1 = $i;
$i++; $temp2 = $i;
echo $temp1 + $temp2;
编辑:在PHP内部邮件列表中指出,在一条语句中使用多个修改变量的操作通常被视为“未定义行为”,而++
则被视为“未定义行为”
因此,PHP5出于实现/优化的原因返回它所做的值是合理的,即使它在逻辑上与对多个语句的合理序列化不一致
(相对较新)包含类似的语言和示例:
除非本规范中明确说明,否则表达式中的操作数相对于彼此的求值顺序未指定。[…](例如,[…]在完整表达式$j=$i+$i++
中,$i
的值是旧的还是新的$i
,未指定。)
可以说,这是一个比“未定义的行为”更弱的说法,因为这意味着它们是按照某种特定的顺序进行评估的,但我们现在开始吹毛求疵了
phpdbg调查(PHP5)
我很好奇,想了解更多关于内部结构的信息,所以使用了一些游戏
无参考资料
用$j=$i
代替$j=&$i
运行代码时,我们从共享一个地址的两个变量开始,refcount为2(但没有is_ref标志):
但一旦您预增量,ZVAL就被分离,并且只有一个temp var与$i共享,因此refcount为2:
Address Refs Type Variable
0x7f189f9ecfc8 2 (integer) $i
0x7f189f859be8 1 (integer) $j
参考转让
当变量绑定在一起时,它们共享一个地址(refcount为2)和一个by ref标记:
Address Refs Type Variable
0x7f9e04ee7fd0 2 (integer) &$i
0x7f9e04ee7fd0 2 (integer) &$j
在预增量之后(但在加法之前),同一地址的refcount为4,表示2个临时变量被引用错误绑定:
Address Refs Type Variable
0x7f9e04ee7fd0 4 (integer) &$i
0x7f9e04ee7fd0 4 (integer) &$j
问题的根源
深入了解上的源代码,我们可以找到ZEND_PRE_INC
opcode的实现:
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,它跟随一系列宏,基本上意味着“增加其refcount”,然后再将其指定为结果
如果我们早些时候创建了一个新的zval,这很好-我们的refcount现在是2,实际变量为1,运算结果为1。但是如果我们决定不这样做,因为我们需要保持一个引用,我们只是增加现有的引用计数,并指向一个可能会再次更改的zval
PHP7
那么PHP7有什么不同呢?好几件事
首先,phpdbg输出相当无聊,因为在PHP7中整数不再是引用计数;相反,引用赋值会创建一个额外指针,指向内存中的同一地址,即实际整数,该指针本身的refcount为1。phpdbg输出如下所示:
Address Refs Type Variable
0x7f175ca660e8 1 integer &$i
int (2)
0x7f175ca660e8 1 integer &$j
int (2)
第二,整数有一个特殊的代码路径:
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\u copy\u value
)
如果它是一个引用,我们将不会命中该代码,但我们没有将引用绑定在一起,而是有以下两行代码:
ZVAL_DEREF(var_ptr);
SEPARATE_ZVAL_NOREF(var_ptr);
第一行写着“如果它是一个参考,跟随它到它的目标”;这使我们从“对整数的引用”到整数本身。第二种——我认为——是“如果它是被重新计算过的东西,并且有多个引用,那么创建一个副本”;在我们的例子中,这将不起任何作用,因为整数不关心refcounts
现在我们有了一个可以递减的整数,它将影响所有按引用关联,但对refcounted类型不会影响按值关联。最后,如果我们想要增量的返回值,我们再次复制它,而不仅仅是分配它;这一次使用稍微不同的宏,它将在必要时增加新zval的refcount:
ZVAL_COPY(EX_VAR(opline->result.var), var_ptr);
免责声明:我不是PHP内部专家(还不是?),所以这一切都是我的理解,不能保证100%正确或完整 因此,首先,PHP7的行为——我注意到,HHVM也跟着它——似乎是正确的,PHP5在这里有一个bug。这里不应该有额外的按引用赋值行为,因为无论执行顺序如何,对
+$i
的两个调用的结果都不应该相同
操作码
ZVAL_DEREF(var_ptr);
SEPARATE_ZVAL_NOREF(var_ptr);
ZVAL_COPY(EX_VAR(opline->result.var), var_ptr);