Примечание переводчика.
Никита Попов внёс и продолжает вносить огромный вклад в развитие
языка PHP. Он очень хорошо понимает внутренности движка PHP и в
данной статье он объясняет некоторые особенности работы PHP в плане
порядка вычисления выражений, которые, пожалуй, особо нигде и не
найти. Этой статье около 7 лет и она практически не потеряла
актуальность, однако найти её довольно сложно, потому что её нет в
блоге Никиты Попова, а она опубликована в его gist-ах на гитхабе.
Думаю полезно будет представить её сообществу на русском
языке.
В своём любимом сообществе lolphp на реддит я наткнулся на пост, где люди удивляются результату следующего кода:
<?php$a = 1;$c = $a + $a++;var_dump($c); // int(3)$a = 1;$c = $a + $a + $a++;var_dump($c); // int(3)
Как вы видите, выражения ($a + $a++)
и ($a +
$a + $a++)
дают одинаковый результат, что довольно
неожиданно. Что же здесь происходит?
Приоритет и ассоциативность операторов
Многие люди думают, что порядок вычисления выражения
определяется приоритетом и ассоциативностью операторов, но
это не так. Приоритет и ассоциативность определяют лишь порядок
группировки операций в выражении.
В первом выражении $c = $a + $a++;
пост-инкремент "++"
имеет более высокий приоритет, чем "+", поэтому "$a++" является
отдельной группой:
$c = $a + ($a++);
Во втором выражении $c = $a + $a + $a++;
пост-инкремент "++" снова имеет более высокий приоритет, нежели
"+":
$c = $a + $a + ($a++);
И "+" является лево-ассоциативным оператором, поэтому левый "+" формирует отдельную группу:
$c = ($a + $a) + ($a++);
Примечание переводчика: код из оригинальной статьи представлял примеры выражений и не являлся валидным, поэтому он был несколько скорректирован.
Что же это говорит нам о порядке вычисления? Ничего. Приоритет и
ассоциативность операторов определяют группировку, но они не
говорят о том, в каком порядке эти группы будут выполняться. В
частности, в последнем примере, как ($a + $a)
, так и
($a++)
может быть выполнено в первую очередь.
В PHP на самом деле не определено что же произойдет. Одна версия PHP может выдать вам один результат, а другая другой. Не пишите код, который зависит от какого-то определенного порядка вычисления выражения.
CV оптимизации
Все-таки, даже не смотря на то, что PHP не определяет порядок вычисления, будет интересно выяснить, почему же мы получаем довольно неожиданный результат в первом выражении (он будет одинаковым во всех последних версиях PHP).
Причина такого результата кроется в оптимизации компилируемых
переменных (compiled variables, CV), которая была добавлена в PHP
5.1. Данная оптимизация по сути позволяет простым переменным
(например, $a
, но не
$a->b
или $a['b']
) напрямую быть
операндами опкодов. Опкоды это то, что PHP генерирует из вашего
скрипта и то, что выполняет Zend VM (виртуальная машина Zend).
Каждый опкод имеет максимум 2 операнда и опциональный
результат.
А теперь давайте посмотрим на опкоды, сгенерированные двумя
примерами кода.
Начнем с $a + $a + $a++
:
// code:$a = 1;$c = ($a + $a) + ($a++);
// opcodes: ASSIGN $a, 1$tmp_1 = ADD $a, $a$tmp_2 = POST_INC $a$tmp_3 = ADD $tmp_1, $tmp_2 ASSIGN $c, $tmp_3
Сгенерированные опкоды получаются довольно понятными:
- сначала идет присваивание
$a = 1
, - далее сложение
$a + $a
с сохранением результата во временную переменную$tmp_1
, - затем выполняется пост-инкремент
$a
с сохранением результата в$tmp_2
, - и, наконец, сложение обоих временных переменных и присвоение
результата переменной
$c
.
Вычисление здесь происходит слева направо (сначала выполняется
$a + $a
, затем $a++
), как вы, наверное, и
ожидали.
А теперь давайте рассмотрим вариант $a + $a++
:
// code:$a = 1;$c = $a + ($a++);
// opcodes: ASSIGN $a, 1$tmp_1 = POST_INC $a$tmp_2 = ADD $a, $tmp_1 ASSIGN $c, $tmp_2
Как вы видите, в этом случае POST_INC ($a++)
выполянется в первую очередь, и значение $a
считывается напрямую опкодом ADD
. Почему? Потому что
чтение значения переменной не требует дополнительных опкодов. Любой
опкод умеет считывать значения простых переменных. Это и есть CV
оптимизация.
Когда не происходит CV оптимизация
Примечание переводчика: в оригинальной статье Никита Попов
рассматривает вариант, когда CV оптимизации не срабатывают из-за
использования оператора подавления ошибок @
. Но это
происходит лишь в PHP 5.x, а в PHP 7 оптимизации работают в том
числе и в этом случае. Поэтому перевод оригинала, применимый только
к PHP 5 убран в спойлер, а мы же рассмотрим вариант, когда код из
примеров работает по-другому из-за отсутствия CV оптимизаций в силу
не использования CV.
Существуют некоторые (редкие) случаи, когда CV оптимизация не
выполняется, например, в случае использования оператора подавления
ошибок @
.
Давайте проверим. Мы снова возьмем выражение $a +
$a++
, но в этот раз погасим ошибки, используя оператор
@
:
<?php$a = 1;@ $c = $a + $a++;var_dump($c); // int(2)
Теперь, когда мы применили оператор подавления ошибок, результат неожиданно поменялся с 3 на 2. И чтобы выяснить почему, давайте взглянем на опкоды:
ASSIGN $a, 1$tmp_1 = BEGIN_SILENCE$var_3 = FETCH_R 'a'$tmp_4 = POST_INC $a$tmp_5 = ADD $var_3, $tmp_4$var_2 = FETCH_W 'c' ASSIGN $var_2, $tmp_5 END_SILENCE $tmp_1
Как мы видим, несколько вещей здесь изменились. Во-первых, весь
код сейчас обрамлён опкодами BEGIN_SILENCE
и
END_SILENCE
для обработки подавления ошибок. Они на
самом деле нам не сильно интересны. Во-вторых, обращение к
переменным $a
и $b
сейчас происходит при
помощи FETCH_R
(для считывания) и FETCH_W
(для записи) вместо прямого использования их в качестве
операндов.
Именно потому, что обращение к $a
сейчас имеет
отдельный опкод, оно будет происходить до инкремента и результат
будет другим.
CV оптимизация не выполняется, например, в случае обращения к элементам массивов или свойствам объектов.
Давайте проверим. Мы снова возьмем выражение $a +
$a++
, но в этот раз будем использовать обращение к элементу
массива вместо переменной:
<?php$a = [1];$c = $a[0] + $a[0]++;var_dump($c); // int(2)
Теперь, когда мы начали использовать массив, результат неожиданно поменялся с 3 на 2. И чтобы выяснить почему, давайте взглянем на опкоды:
ASSIGN $a, [1]$tmp_3 = FETCH_DIM_R 'a', 0$var_4 = FETCH_DIM_RW 'a', 0$tmp_5 = POST_INC $var_4$tmp_6 = ADD $tmp_3, $tmp_5 ASSIGN $c, $tmp_6
Как мы видим, обращение к элементу массива здесь происходит при
помощи FETCH_DIM_R
(для считывания) и
FETCH_DIM_RW
(для чтения/записи) вместо прямого
использования его в качестве операндов.
Именно потому, что получение операндов для сложения сейчас имеет отдельный опкод, оно будет происходить до инкремента, и результат будет другим.
На самостоятельное изучение оставлю аналогичный пример, но с использованием объектов. Найти пример кода можно по этой ссылке на 3v4l.org.
Выводы
Если делать какие-то выводы из всего этого, то я думаю они должны быть такими:
- Не нужно полагаться на порядок вычисления выражений. Он не определен.
- Оператор
@
отключает CV оптимизации и в результате ухудшает производительность. Оператор@
в принципе плохо сказывается на производительности.
Примечание переводчика: как было сказано выше,
@
отключает CV оптимизации только в 5.x, в PHP 7 CV
оптимизации имеют место даже в случае использования оператора
подавления ошибок (но возможно, это происходит не во всех случаях).
У Никиты Попова в блоге есть интересная статья Static Optimization in PHP 7, на случай, если
кто-то хочет глубже копнуть тему оптимизации.