我想分享的第一件事是将其转换为 MIPS 的复杂性仅来自于函数调用的存在,而不是因为涉及递归——fact 是递归的,恕我直言是红色鲱鱼。为此,我将说明一个非递归函数,它具有您所说的递归函数的每一点复杂性:
int fact (int n)
{
if (n < 1) return 0;
else return n * other(n - 1); // I've changed the call to "fact" to function "other"
}
我的改变不再是递归的!但是,此版本的 MIPS 代码看起来与您的 fact 的 MIPS 代码相同(当然,除了 jal fact 会更改 jal other)。这是为了说明翻译 this 的复杂性是由于函数内的调用,与被调用的对象无关。 (虽然 YMMV 具有优化技术。)
要理解函数调用,你需要理解:
- 程序计数器:程序如何与程序计数器交互,尤其是在函数调用的上下文中。
- 参数传递
- 注册约定,一般
在 C 中,我们有明确的参数。当然,这些显式参数也出现在汇编/机器语言中——但也有在机器代码中传递的参数在 C 代码中不可见。例如返回地址值和堆栈指针。
这里需要的是对函数的分析(与递归无关):
参数n 将在函数入口的$a0 中。 n 的值在函数调用(到 other)之后是必需的,因为在函数调用返回 * 的右手操作数之前我们不能相乘。
因此,n(* 的左侧操作数)必须在对 other 的函数调用中幸存下来,而在 $a0 中则不会——因为我们自己的代码将重新利用 $a0 来调用other(n-1),因为n-1 必须为此进入$a0。
此外,(在 C 中,隐式)参数$ra 保存返回给调用者所需的返回地址值。同样,对other 的调用将重新利用$ra 寄存器,清除其先前的值。
因此,此函数(您的或我的)需要两个值才能在其主体内的函数调用(例如对 other 的调用)中继续存在。
解决方案很简单:我们需要的值(存在于寄存器中,这些值被我们正在做的事情或被调用者可能做的事情重新利用或清除)需要移动或复制到其他地方:在函数中存在的地方打电话。
内存可用于此目的,并且我们可以使用堆栈为这些目的获取一些内存。
基于此,我们需要在调用other 之后创建一个堆栈帧,该堆栈帧为我们需要的两件事(否则会被清除)留有空间。必须保存条目$ra(并稍后重新加载)以便我们使用它返回;此外,需要保存初始的n 值,以便我们可以将其用于乘法。 (堆栈帧通常在函数序言中创建,并在函数尾声中删除。)
正如机器代码(甚至一般的编程)中经常出现的情况,还有其他处理事物的方法,尽管要点是相同的。 (这是一件好事,优化编译器通常会根据特定情况寻求最佳方式。)
递归的存在或不存在不会改变我们需要将其翻译成汇编/机器语言的基本分析。递归极大地增加了堆栈溢出的可能性,但不会改变这种分析。
附录
需要明确的是,递归强制要求使用可动态扩展的调用堆栈——尽管所有现代计算机系统都提供了这样一个调用堆栈,因此在当今的系统上很容易忘记或掩盖这一要求。
对于没有递归的程序,调用堆栈不是必需的——局部变量可以分配给函数私有的全局变量(包括返回地址),这是在某些旧系统上完成的,比如 PDP-8,它确实做到了不为调用堆栈提供特定的硬件支持。
使用堆栈内存来传递参数和/或寄存器不足的系统可能不需要此答案中描述的分析,因为变量已经存储在嵌套函数调用的内存中。
现代寄存器丰富的机器上的寄存器分区创造了上述分析的要求。这些寄存器丰富的机器(大部分)在 CPU 寄存器中传递参数和返回值,这很有效,但有时需要进行复制,因为寄存器从一个函数重新用于另一个函数。