0

I started to read MIPS to understand better how my C++ and C code works under the computer skin. I started with a recursive function, a Fibonacci function. The C code is:

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

MIPS code:

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

When n>1 it goes until the code reaches the first jal instruction. What happens next? it return to fib label ignoring the code below (the fib(n-2) call will never be executed?)? If that happens, the $sp pointer decreases 3 words again and the cycle will go until n<=1. I can't understand how this works when first jal instruction is reached.

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
Pedro R.
  • 93
  • 2
  • 9
  • It eventually returns to the return address after `jal`. Also, remember that real MIPS has branch-delay slots. If you want GCC to only fill them with NOPs instead of real instructions (so the code would run the same on a simplified MIPS without), use `-fno-delayed-branch`. [Tweak mips-gcc output to work with MARS](https://stackoverflow.com/q/13052444). Also, if you compile with `gcc -Og -fverbose-asm`, that might help to annotate the asm with comments. [How to remove "noise" from GCC/clang assembly output?](https://stackoverflow.com/q/38552116) – Peter Cordes Mar 31 '22 at 21:06

1 Answers1

0

Can you follow how the recursion works in C?

In some sense, recursion has two components: the forward part and the backward part.  In the forward part, a recursive algorithm computes things before the recursion, and in the backward part, a recursive algorithm computes things after the recursion completes.  In between the two parts, there is the recursion.

See this answer: https://stackoverflow.com/a/71551098/471129

Fibonacci is just slightly more complicated as it performs recursion twice, not just once as in the above list printing example.

However, the principles are the same:  There is work done before the recursion, and work done after (either of which can be degenerate).  The before part happens as code in front of the recursion executes, and the recursion builds up stack frames that are placeholders for work after the recursion yet to be completed.  The after part happens as the stack frames are released and the code after the recursive call is executed.

In any given call chain, the forward part goes until n is 0 or 1, then the algorithm starts returning back to the stacked callers, for whom the backward part kicks in unwinding stack frames until it returns to the original caller (perhaps main) rather than to some recursive fib caller.&npsp; Again, complicated by use of two recursive invocations rather than one as in simpler examples.

With fib, the work done before is to count down (by -1 or -2) until reaching 0 or 1.  The work done after the recursion is to sum the two prior results.  The recursion itself effectively suspends an invocation or activation of fib with current values, to be resumed when a recursive call completes.


Recursion in MIPS algorithm is the same; however, function operations are spread out over several machine code instructions that are implicit in C.


Suggest single stepping over a call to fib(2) as a very small example that may help you see what's going on there.  Suggest first doing this in C — single step until the outer fib call has full completed and returned to the calling test function (e.g. main).

To make the C version just a bit easier to view in the debugger you might use this version:

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

With that equivalent C version, you'll be able to inspect fm1, fm2, and result during single stepping.  That will make it easier to follow.

Next, do the same in the assembly version.  Debug single step to watch execution of fib(2), and draw parallels with the equivalents in C.


There's another way to think about recursion, which is ignore the recursion, pretending that the recursive call is to some unrelated function implementation that just happens to yield the proper results of the recursive function; here's such a non-recursive function:

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

With this code, and the assumption that fibX simply works correctly to return proper results, you can focus strictly on the logic of one level, namely, the body of this fib, without considering the recursion at all.

Note that we can do the same in assembly language — though the opportunities for errors / typos are always much larger than in the C, since you still have to manipulate stack frames and preserve critical storage for later use after the calling.


The code you've posted has a transcription error, making it different from the C version.  It is doing the C equivalent of:

return fib(n-1) + fib(n-1);
Erik Eidt
  • 23,049
  • 2
  • 29
  • 53