如何在自定义虚拟机中实现尾调用?
我知道我需要弹出原始函数的本地堆栈,然后是参数,然后推送新参数。但是,如果我弹出函数的本地堆栈,我应该如何推送新参数?他们刚刚从堆栈中弹出。
如何在自定义虚拟机中实现尾调用?
我知道我需要弹出原始函数的本地堆栈,然后是参数,然后推送新参数。但是,如果我弹出函数的本地堆栈,我应该如何推送新参数?他们刚刚从堆栈中弹出。
我想当然地认为我们在这里讨论的是传统的“基于堆栈”的虚拟机。
您弹出当前函数的本地堆栈,保留非堆栈“寄存器”中仍然相关的部分(其中“相关部分”显然是即将到来的递归尾调用的参数),然后(一旦所有函数的本地堆栈和参数被清理)你推送递归调用的参数。例如,假设您正在优化的功能类似于:
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;
}