Skip to content

Instantly share code, notes, and snippets.

@ksimka
Created September 25, 2014 09:45
Show Gist options
  • Save ksimka/33a275f5984d805c3161 to your computer and use it in GitHub Desktop.
Save ksimka/33a275f5984d805c3161 to your computer and use it in GitHub Desktop.

Igor Wiedler написал простенькую функцию retry, которая повторяет выполнение коллбека до получения успешного результата или достижения заданного количества неудач. При этом он использовал goto для реализации цикла. Когда его спросили, а почему goto, а не, скажем, рекурсия, он неожиданно очень подробно и интересно ответил. Ниже приводится перевод его ответа.

Конечно же, я рассматривал альтернативы goto. Я очень подробно их изучил, и рад представить вам результаты.

Когда парсер PHP читает исходник, код компилируется в последовательность опкодов, которая затем будет выполнена движком Zend (tm) (r). Компилятор выполняет кое-какие оптимизации, но вообще он довольно тупой. Поэтому, в зависимости от написанного вами кода он будет генерировать разные опкоды. Это напрямую влияет на производительность.

Существует несколько способов написать цикл. Начнём с упомянутого вами — рекурсии.

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», хвостовые рекурсии (tail call — прим. пер.) могут быть скомпилированы в инструкции весьма эффективно. Однако, компилятор PHP не использует преимущества этой техники, поэтому вызовы функций довольно дорогие.

Попробуем улучшить ситуацию при помощи цикла while.

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. Всё остальное работает слишком медленно.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment