PHP: копирование по записи и присваивание по ссылке выполняют разные действия на PHP5 и PHP7

У нас есть кусок простого кода:

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 , но я понятия не имел.

Любая помощь будет большой, спасибо заранее.

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 #* EIO 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 (пока?), Так что это все из моего понимания и не гарантируется на 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 было указано, что использование нескольких операций, которые изменяют переменную в рамках одного оператора, обычно считается «неопределенным поведением», а ++ используется в качестве примера этого в C / C ++ .

Таким образом, для PHP 5 разумно вернуть значение, которое оно делает для целей реализации / оптимизации, даже если оно логически несовместимо с последовательной сериализацией в несколько операторов.

(Относительно новая) спецификация языка PHP содержит похожие язык и примеры:

Если не указано явно в этой спецификации, порядок, в котором операнды в выражении оцениваются относительно друг друга, не определен. […] (Например, […] в полном выражении $j = $i + $i++ , независимо от того, является ли значение $i старым или новым $i , не определено.)

Вероятно, это более слабое требование, чем «неопределенное поведение», поскольку подразумевается, что они оцениваются в определенном порядке, но теперь мы собираем nit-picking.

Исследование phpdbg (PHP 5)

Мне было любопытно, и я хочу узнать больше о внутренних компонентах , так же как некоторые из них используют phpdbg .

Нет ссылок

Запустив код с помощью $j = $i вместо $j =& $i , мы начнем с 2 переменных, разделяющих адрес, с refcount из 2 (но без флага is_ref):

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

Но как только вы предварительно увеличиваете, zvals разделяются, и только один temp var делится с $ i, давая refcount 2:

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

С ссылочным назначением

Когда переменные связаны вместе, они делят адрес с refcount of 2 и маркером by-ref:

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

После предварительных приращений (но перед добавлением) один и тот же адрес имеет коэффициент пересчета 4, показывающий 2 temp vars, ошибочно связанные ссылкой:

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

Источник вопроса

Копаясь в источнике на http://lxr.php.net , мы можем найти реализацию ZEND_PRE_INC операции 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, который после целого ряда макросов в основном означает «увеличивать его refcount», прежде чем назначать его в качестве результата.

Если мы создали новый zval раньше, это нормально – наш refcount теперь составляет 2, 1 для фактической переменной, плюс 1 для результата операции. Но если мы решили не делать этого, потому что нам нужно было провести ссылку, мы просто увеличиваем существующий счетчик ссылок и указываем на zval, который может снова быть изменен.

PHP 7

Так что же изменилось в PHP 7? Несколько вещей!

Во-первых, вывод phpdbg довольно скучный, потому что целые числа больше не ссылаются на PHP 7; вместо этого ссылочное присваивание создает дополнительный указатель, который сам имеет 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 ), мы можем просто IS_REFERENCE его. Если нам понадобится возвращаемое значение, мы можем скопировать его значение в результат ( ZVAL_COPY_VALUE ).

Если это ссылка, мы не будем ударять по этому коду, а вместо того, чтобы связывать ссылки, мы имеем следующие две строки:

 ZVAL_DEREF(var_ptr); SEPARATE_ZVAL_NOREF(var_ptr); 

В первой строке говорится: «Если это ссылка, следуйте ее цели»; это выводит нас из нашей «ссылки на целое» на само целое. Второй – я думаю – говорит: «если это что-то пересчитанное и имеет несколько ссылок, создайте его копию»; в нашем случае это ничего не сделает, потому что целое число не заботится о пересчетах.

Итак, теперь у нас есть целое число, которое мы можем уменьшить, что повлияет на все ассоциации ссылок, но не на значения для refcounted типов. Наконец, если мы хотим вернуть значение приращения, мы снова скопируем его, а не просто присваиваем его; и на этот раз с немного другим макросом, который при необходимости увеличит пересчет нашего нового zval:

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

Я бы сказал, что он работает на PHP7, это правильный путь. Плохо неявно изменять способ работы операторов, зависящий от того, ссылается ли операнд где-нибудь или нет.

Это самая лучшая вещь о том, что PHP7 полностью переписан: никакого кодового / ошибка-разработки v4 / v5 кода не будет работать.