如何在自定义虚拟机中实现尾调用

发布于 2024-09-01 09:55:36 字数 105 浏览 3 评论 0原文

如何在自定义虚拟机中实现尾调用?

我知道我需要弹出原始函数的本地堆栈,然后弹出它的参数,然后推送新参数。但是,如果我弹出函数的本地堆栈,我该如何推送新参数呢?它们刚刚从堆栈中弹出。

How can I implement tail calls in a custom virtual machine?

I know that I need to pop off the original function's local stack, then it's arguments, then push on the new arguments. But, if I pop off the function's local stack, how am I supposed to push on the new arguments? They've just been popped off the stack.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

猫烠⑼条掵仅有一顆心 2024-09-08 09:55:36

我想当然地认为我们在这里讨论的是传统的“基于堆栈”的虚拟机。

您弹出当前函数的本地堆栈在非堆栈“寄存器”中保留仍然相关的部分(其中“相关部分”显然是即将进行的递归尾调用的参数),然后(一旦清除了函数的所有本地堆栈和参数),您就可以推送递归调用的参数。例如,假设您正在优化的函数类似于:

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 是正常的弹出,只是丢弃堆栈中的 top n 条目,但 POP_KEEP n 是一种变体,将它们保留在“某处”(例如,在应用程序无法直接访问的辅助堆栈中,而只能由 VM 访问)自己的机器——在讨论 VM 实现时,具有此类字符的存储有时称为“寄存器”)和匹配的 PUSH_KEPT n ,它将“寄存器”清空回 VM 的正常堆栈。

I take it for granted that we're discussing a traditional "stack-based" virtual machine here.

You pop off the current function's local stack preserving the still-relevant parts in non-stack "registers" (where the "relevant parts" are, clearly, the argument for the forthcoming recursive tail call), then (once all of the function's local stack and arguments are cleaned up) you push the arguments for the recursive call. E.g., suppose the function you're optimizing is something like:

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

which without optimization might produce byte-code symbolically like:

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

the CALL_FUN2 means "call a function with two arguments". With the optimization, it could become sometime like:

   POP_KEEP     2
   POP_DISCARD  2
   PUSH_KEPT    2
   JUMP         AUX

Of course I'm making up my symbolic bytecodes as I go along, but I hope the intent is clear: POP_DISCARD n is the normal pop that just discards the top n entries from the stack, but POP_KEEP n is a variant that keeps them "somewhere" (e.g. in an auxiliary stack not directly accessible to the application but only to the VM's own machinery -- storage with such a character is sometimes called "a register" when discussing VM implementation) and a matching PUSH_KEPT n which empties the "registers" back into the VM's normal stack.

蝶舞 2024-09-08 09:55:36

我认为你看待这个问题的方式是错误的。不要将旧变量从堆栈中弹出,然后推送新变量,只需重新分配已经存在的变量(小心地)即可。如果您将代码重写为等效的迭代算法,这与会发生的优化大致相同。

对于此代码:

 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;
} 

I think you're looking at this the wrong way. Instead of popping the old variables off the stack and then pushing the new ones, simply reassign the ones already there (carefully). This is roughly the same optimization that would happen if you rewrote the code to be the equivalent iterative algorithm.

For this code:

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

would be

 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"

There's no need to pop or push anything on the stack, merely reassign.
Clearly, this can be further optimized, by putting the exit condition second, allowing us to skip a jump, resulting in fewer operations.

 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

Looking again, this "assembly" better reflects this C++, which clearly has avoided the recursion calls

int fact(int x, int total=1)
    for( ; x>1; --x)
        total*=x;
    return total;
} 
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文