Как реализовать хвостовые вызовы в пользовательской виртуальной машине

Как я могу реализовать хвостовые вызовы в пользовательской виртуальной машине?

Я знаю, что мне нужно извлечь локальный стек исходной функции, затем ее аргументы, а затем выдвинуть новые аргументы. Но если я вытолкну из локального стека функции, как я должен выдвинуть новые аргументы? Они только что выскочили из стека.

2 ответа

Решение

Я считаю само собой разумеющимся, что здесь мы обсуждаем традиционную "основанную на стеке" виртуальную машину.

Вы вытаскиваете из локального стека текущей функции, сохраняя все еще релевантные части в не-стековых "регистрах" (где "релевантные части", несомненно, являются аргументом для предстоящего рекурсивного вызова хвоста), затем (когда все локальные функции функции стек и аргументы очищены) вы выдвигаете аргументы для рекурсивного вызова. Например, предположим, что оптимизируемая вами функция выглядит примерно так:

def aux(n, tot):
  if n <= 1: return tot
  return aux(n-1, tot * n)

который без оптимизации может создать байт-код символически, например:

AUX:   LOAD_VAR   N
       LOAD_CONST 1
       COMPARE
       JUMPIF_GT  LAB
       LOAD_VAR   TOT
       RETURN_VAL
LAB:   LOAD_VAR   N
       LOAD_CONST 1
       SUBTRACT
       LOAD_VAR   TOT
       LOAD_VAR   N
       MULTIPLY
       CALL_FUN2  AUX
       RETURN_VAL

CALL_FUN2 означает "вызвать функцию с двумя аргументами". С оптимизацией это могло бы когда-то походить на:

   POP_KEEP     2
   POP_DISCARD  2
   PUSH_KEPT    2
   JUMP         AUX

Конечно, я создаю свои символические байт-коды по мере продвижения, но я надеюсь, что цель ясна: POP_DISCARD n это нормальный поп, который просто отбрасывает верх n записи из стека, но POP_KEEP n это вариант, который хранит их "где-то" (например, во вспомогательном стеке, не доступном непосредственно для приложения, но только для собственного механизма виртуальной машины - хранилище с таким символом иногда называют "регистром" при обсуждении реализации виртуальной машины) и соответствие PUSH_KEPT n который очищает "регистры" обратно в обычный стек виртуальной машины.

Я думаю, что вы смотрите на это неправильно. Вместо того, чтобы выталкивать старые переменные из стека и затем выталкивать новые, просто переназначьте уже существующие (осторожно). Это примерно та же самая оптимизация, которая произошла бы, если бы вы переписали код, чтобы он стал эквивалентным итерационным алгоритмом.

Для этого кода:

 int fact(int x, int total=1) {
     if (x == 1)
         return total;
     return fact(x-1, total*x);
 }

было бы

 fact:
   jmpne x, 1, fact_cont  # if x!=1 jump to multiply
   retrn total            # return total
 fact_cont:               # update variables for "recursion
   mul total,x,total      # total=total*x
   sub x,1,x              # x=x-1
   jmp fact               #"recurse"

Нет необходимости вставлять или вставлять что-либо в стек, просто переназначать.
Очевидно, что это может быть дополнительно оптимизировано, если поставить условие выхода вторым, что позволит нам пропустить переход, что приведет к меньшему количеству операций.

 fact_cont:               # update variables for "recursion
   mul total,x,total      # total=total*x
   sub x,1,x              # x=x-1
 fact:
   jmpne x, 1, fact_cont  # if x!=1 jump to multiply
   retrn total            # return total

Опять же, эта "сборка" лучше отражает этот C++, который явно избегает рекурсивных вызовов.

int fact(int x, int total=1)
    for( ; x>1; --x)
        total*=x;
    return total;
} 
Другие вопросы по тегам