如何在自定义虚拟机中实现尾调用?
我知道我需要弹出原始函数的本地堆栈,然后是它的参数,然后推送新的参数。但是,如果我弹出函数的本地堆栈,我该如何推动新的参数呢?他们刚从堆栈中弹出。
答案 0 :(得分:4)
我认为我们在这里讨论传统的“基于堆栈”的虚拟机是理所当然的。
弹出当前函数的本地堆栈,保留非堆栈“寄存器”中仍然相关的部分(其中“相关部分”显然是即将发生的递归尾调用的参数),然后(一旦所有函数的本地堆栈和参数被清理),你推送递归调用的参数。例如,假设您正在优化的功能类似于:
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
是普通的pop,它只丢弃堆栈中的顶级n
条目,但是POP_KEEP n
是一种变体,它将它们“保持在某个地方”(例如,在一个不能直接访问应用程序但只能访问VM自己的机器的辅助堆栈中 - 具有这种字符的存储在讨论时有时称为“寄存器” VM实现)和匹配的PUSH_KEPT n
,它将“寄存器”清空回VM的正常堆栈。
答案 1 :(得分:1)
我认为你正在以错误的方式看待这个问题。不要将旧变量从堆栈中弹出然后推出新变量,而只需重新分配已存在的变量(小心)。如果您将代码重写为等效的迭代算法,这与大致相同的优化。
对于此代码:
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;
}