assembly 递归Fibonacci MIPS

qxsslcnc  于 11个月前  发布在  其他
关注(0)|答案(1)|浏览(121)

我开始阅读MIPS,以更好地理解我的C++和C代码在计算机 shell 下的工作方式。我从递归函数开始,Fibonacci函数。C代码是:

int fib(int n) {
if(n == 0) { return 0; }
if(n == 1) { return 1; }
return (fib(n - 1) + fib(n - 2));
}

字符串
MIPS代码:

fib:
addi $sp, $sp, -12 
sw $ra, 8($sp) 
sw $s0, 4($sp)

addi $v0, $zero, $zero
beq $a0, $zero, end
addiu $v0, $zero, 1
addiu $t0, $zero, 1 
beq $a0, $t0, end
addiu $a0, $a0, -1
sw $a0, 0($sp) 
jal fib #fib(n-1)
addi $s0, $v0, $zero 
lw $a0, 0($sp) 
addiu $a0, $a0, -1
jal fib #fib(n-2)
add $v0, $v0, $s0

end:
lw $s0, 4($sp)
lw $ra, 8($sp) 
addi $sp, $sp, 12
jr $ra


n>1时,它会一直运行,直到代码到达第一个jal指令。接下来会发生什么?它会返回到fib标签,忽略下面的代码(fib(n-2)调用永远不会被执行?)?如果发生这种情况,$sp指针再次减少3个字,循环将持续到n<=1。我不明白当第一个jal指令到达时这是如何工作的。

gwbalxhn

gwbalxhn1#

你能理解C语言中的递归是如何工作的吗?
从某种意义上讲,递归有两个部分:前向部分和后向部分。在前向部分,递归算法在递归之前进行计算,而在后向部分,递归算法在递归完成之后进行计算。在这两个部分之间,存在递归。
请参见此答案:https://stackoverflow.com/a/71551098/471129
斐波那契只是稍微复杂一点,因为它执行递归两次,而不是像上面的列表打印示例中那样只执行一次。
但是,原则是相同的:递归之前有工作,递归之后也有工作在递归前面的代码执行时发生before部分,并且递归建立堆栈帧,这些堆栈帧是递归之后尚未完成的工作的占位符。处决了被
在任何给定的调用链中,前向部分一直进行到n为0或1,然后算法开始返回到堆栈调用者,对于堆栈调用者,后向部分开始展开堆栈帧,直到它返回到原始调用者(可能是main),而不是返回到某个递归的fib调用者。
使用fib,之前的工作是递减计数(-1或-2),直到达到0或1。递归之后的工作是对之前的两个结果求和。递归本身有效地挂起了对fib的调用或激活,以便在递归调用完成时恢复。
MIPS算法中的递归是相同的;然而,函数操作分散在C语言中隐含的几个机器代码指令上。
建议单步执行一个fib(2)调用,作为一个很小的例子,它可以帮助你了解那里发生了什么。建议首先用C语言单步执行,直到外部的fib调用完全完成并返回到调用测试函数(例如main)。
为了使C版本在调试器中更容易查看,您可以使用此版本:

int fib(int n) {
    if (n == 0) { return 0; }
    if (n == 1) { return 1; }
    int fm1 = fib(n-1);
    int fm2 = fib(n-2);
    int result = fm1 + fm2;
    return result;
}

字符串
使用等效的C版本,您将能够在单步执行期间检查fm1fm2result,这将使其更易于理解。
接下来,在程序集版本中进行同样的操作。调试单步调试以监视fib(2)的执行,并与C中的等效程序进行比较。
还有另一种方式来考虑递归,那就是忽略递归,假设递归调用是对某个不相关的函数实现的,而这个函数实现恰好产生了递归函数的正确结果;下面是这样一个非递归函数:

int fib(int n) {
    if (n == 0) { return 0; }
    if (n == 1) { return 1; }
    int fm1 = fibX(n-1);   // calls something else that computes fib(n-1)
    int fm2 = fibX(n-2);   //   "
    int result = fm1 + fm2;
    return result;
}


使用这段代码,并假设fibX * 能够正确工作并返回正确的结果 *,您可以严格地关注一个级别的逻辑,即fib的主体,而根本不考虑递归。
请注意,我们可以在汇编语言中做同样的事情--尽管错误/打字错误的机会总是比在C语言中大得多,因为您仍然必须操作堆栈帧并保留关键存储以备调用后使用。
您发布的代码有一个转录错误,这使得它与C版本不同。它在执行C版本的等效操作:

return fib(n-1) + fib(n-1);


让我们回到这个更明确的版本:

int fib(int n) {                  // n defined upon entry
    if (n == 0) { return 0; }
    if (n == 1) { return 1; }
    int fm1 = fib(n-1);           // fm1 defined
    int fm2 = fib(n-2);           // n used to compute n-2
    int result = fm1 + fm2;       // fm1 used
    return result;
}


对于汇编语言,必须对变量进行分析,以便使用适当的寄存器和堆栈帧存储器。
首先,让我们观察一下n在函数调用中是“活的”,也就是fib(n-1)的函数调用。这是一个重要的属性。函数调用会清除某些寄存器,因此保留在函数调用之前定义并在函数调用之后使用的内容非常重要。这里,n的值被定义为一个参数。因此可以认为在输入时已经定义。它用于执行fib(n-1),这并不重要,但是它也用于执行fib(n-2)-计算n-2,该计算发生在调用fib(n-1)之后。因此,n需要特殊的存储空间,以便在第一次函数调用后仍然存在。
函数调用中还有什么是活的?它是fm1,因为它在调用fib(n-2)之前定义,在调用之后使用。所以,fm1也需要函数存活存储。
但还有什么呢?我们在C语言中看不到它,但在汇编语言中,返回地址是作为一个附加的函数参数公开给我们的,就像n一样,它被认为是在函数入口时定义的。传入的参数值(返回地址)必须在两个函数调用中都存在,因为它在最后被用来完成return操作。
无论是使用带有显式变量的版本,还是使用fib(n-1) + fib(n-2)中隐式的内部临时变量的更简单版本,都必须进行这种活动性/存储分析。

让我们进一步注意到,这种分析对于fibX非递归版本是相同的,因为寄存器保留的所有需求都来自函数调用,而不管递归如何。(如果我们知道调用者和被调用者的实现,那么就有机会优化调用约定,但我们选择不在这些学习场景中使用它们)。
因此,我们需要做的是创建一些堆栈空间供函数使用,并在该堆栈空间中存储返回地址$ra和返回地址n,并为变量fm1提供空间(或该版本中的临时)。稍后,fm1需要在定义后存储到内存中,而n需要从内存中重新加载以计算n-2,在第二次递归调用后,fm1可以被重新加载,返回值计算,最后返回地址被重新加载,栈帧被释放,函数可以返回到它的调用者。
我们还要注意的是,fm2result相比之下相对简单,它们都不需要在函数调用中幸存下来,所以它们可以使用调用clobbered寄存器或从它们出现的寄存器中使用。

相关问题