> Igor Wiedler написал [простенькую функцию retry](https://github.com/igorw/retry/blob/master/src/retry.php), которая повторяет выполнение коллбека до получения успешного результата или достижения заданного количества неудач. При этом он использовал goto для реализации цикла. Когда его спросили, а почему goto, а не, скажем, рекурсия, он неожиданно очень подробно и интересно ответил. Ниже приводится перевод его ответа. Конечно же, я рассматривал альтернативы `goto`. Я очень подробно их изучил, и рад представить вам результаты. Когда парсер PHP читает исходник, код компилируется в последовательность опкодов, которая затем будет выполнена движком Zend (tm) (r). Компилятор выполняет кое-какие оптимизации, но вообще он довольно тупой. Поэтому, в зависимости от написанного вами кода он будет генерировать разные опкоды. Это напрямую влияет на производительность. Существует несколько способов написать цикл. Начнём с упомянутого вами — рекурсии. ```php function retry($retries, callable $fn) { try { return $fn(); } catch (\Exception $e) { if (!$retries) { throw new FailingTooHardException('', 0, $e); } retry($retries - 1, $fn) } } ``` Компилятор PHP этот код преобразует в такие опкоды: ``` function name: igorw\retry number of ops: 24 compiled vars: !0 = $retries, !1 = $fn, !2 = $e line # * op fetch ext return operands --------------------------------------------------------------------------------- 7 0 > RECV !0 1 RECV !1 11 2 INIT_FCALL_BY_NAME !1 3 DO_FCALL_BY_NAME 0 $0 4 > RETURN $0 12 5* JMP ->23 6 > CATCH 17 'Exception', !2 13 7 BOOL_NOT ~1 !0 8 > JMPZ ~1, ->17 14 9 > FETCH_CLASS 4 :2 'igorw%5CFailingTooHardException' 10 NEW $3 :2 11 SEND_VAL '' 12 SEND_VAL 0 13 SEND_VAR !2 14 DO_FCALL_BY_NAME 3 15 > THROW 0 $3 15 16* JMP ->17 16 17 > INIT_NS_FCALL_BY_NAME 18 SUB ~5 !0, 1 19 SEND_VAL ~5 20 SEND_VAR !1 21 DO_FCALL_BY_NAME 2 $6 22 > RETURN $6 18 23* > RETURN null ``` Как видите, выходит 24 инструкции. Дороже всего здесь обходятся вызовы функций, ибо каждый аргумент задаётся по отдельности, и есть ещё дополнительная инструкция (`DO_FCALL_BY_NAME`) для собственно вызова функции. На самом деле, в этом нет никакой необходимости. По словам Steele в его статье «[Lambda: The Ultimate GOTO](http://dspace.mit.edu/bitstream/handle/1721.1/5753/AIM-443.pdf)», хвостовые рекурсии (tail call — прим. пер.) могут быть скомпилированы в инструкции весьма эффективно. Однако, компилятор PHP не использует преимущества этой техники, поэтому вызовы функций довольно дорогие. Попробуем улучшить ситуацию при помощи цикла `while`. ```php function retry($retries, callable $fn) { while (true) { try { return $fn(); } catch (\Exception $e) { if (!$retries) { throw new FailingTooHardException('', 0, $e); } $retries--; } } } ``` Вот что говорит на это компилятор: ``` function name: igorw\retry number of ops: 23 compiled vars: !0 = $retries, !1 = $fn, !2 = $e line # * op fetch ext return operands --------------------------------------------------------------------------------- 7 0 > RECV !0 1 RECV !1 9 2 > FETCH_CONSTANT ~0 'igorw%5Ctrue' 3 > JMPZ ~0, ->22 11 4 > INIT_FCALL_BY_NAME !1 5 DO_FCALL_BY_NAME 0 $1 6 > RETURN $1 12 7* JMP ->21 8 > CATCH 15 'Exception', !2 13 9 BOOL_NOT ~2 !0 10 > JMPZ ~2, ->19 14 11 > FETCH_CLASS 4 :3 'igorw%5CFailingTooHardException' 12 NEW $4 :3 13 SEND_VAL '' 14 SEND_VAL 0 15 SEND_VAR !2 16 DO_FCALL_BY_NAME 3 17 > THROW 0 $4 15 18* JMP ->19 16 19 > POST_DEC ~6 !0 20 FREE ~6 18 21 > JMP ->2 19 22 > > RETURN null ``` Уже лучше. Но тут в самом верху есть довольно неэффективная инструкция `FETCH_CONSTANT`. Она требует проверки на наличие константы в неймспейсе: `igorw\true`. Мы можем поправить это, заменив `while (true)` на `while (\true)`. Это позволит избавиться от `FETCH_CONSTANT`, теперь там прямо указана константа `true`: ``` line # * op fetch ext return operands --------------------------------------------------------------------------------- 7 0 > RECV !0 1 RECV !1 9 2 > > JMPZ true, ->21 ``` Но `JUMPZ` с аргументом `true` излишне. `true` не бывает нулём. В идеале нам бы просто убрать эту проверку. PS: с циклом `for (;;)` та же фигня, лишние переходы, поэтому едем дальше. Итак, можем ли мы избавиться от лишнего перехода? Попробуем `do-while`! ``` function name: igorw\retry number of ops: 21 compiled vars: !0 = $retries, !1 = $fn, !2 = $e line # * op fetch ext return operands --------------------------------------------------------------------------------- 7 0 > RECV !0 1 RECV !1 11 2 > INIT_FCALL_BY_NAME !1 ... 15 > THROW 0 $3 15 16* JMP ->17 16 17 > POST_DEC ~5 !0 18 FREE ~5 18 19 > JMPNZ true, ->2 19 20 > > RETURN null ``` Шикарно! Лишний `JMPZ` ушёл! Правда, ценой появления `JMPNZ` в конце. Так будет лучше в случае успешного завершения функции, но в случае необходимости повторения мы по-прежнему будем выполнять лишние переходы, которые вообще-то должны быть безусловными. И есть способ убрать этот последний условный переход: использовать замечательную встроенную в PHP фичу под названием `goto`. С `goto` мы получаем тот же набор опкодов как и с `do...while`, но последний переход становится безусловным! Вот так вот. Это наиболее эффективный способ писать безусловные циклы в PHP. Всё остальное работает слишком медленно.