The UNIVERSITY of NORTH CAROLINA at CHAPEL HILL Problem 1. Stack Detective Comp 411 Computer Organization Fall 2006 Solutions for Problem Set #4 Consider the following recursive C function to compute the n th Fibonacci number. int fib(int n) { if (n < 2) return n; else return fib(n-1) + fib(n-2); After compiling, the following assembly code is generated: fib: L01: addu $sp,$sp,-12 L02: sw $ra,8($sp) L03: sw $a0,4($sp) L04: slti $t0,$a0,2 L05: beq $t0,$0,l08 L06: add $v0,$0,$a0 L07: beq $0,$0,L16 L08: addi $a0,$a0,-1 L09: jal fib L10: sw $v0,($sp) L11: lw $a0,4($sp) L12: addi $a0,$a0,-2 L13: jal fib L14: lw $t0,($sp) L15: addu $v0,$v0,$t0 L16: lw $ra,8($sp) L17: addu $sp,$sp,12 L18: j $ra a) Explain how each of the 3 words allocated on the stack are used? Could this number be reduced? If so, explain how, if not explain why. (5 pts) The return address is saved in $sp-8, the argument n is saved in $sp-4, and the value returned from the first call to fib (with n-1) is saved in $sp. All of these values must be saved; the return address must be saved because the function is not a leaf. The n argument must be saved because it is needed to construct the parameter for the second call to fib (with n-2). The value returned from the first fib call must be saved as well, so that it is available after the second call. b) Suppose that the statement labeled L10 was replaced with add $a1,$0,$v0, and the two statements labeled L14 and L15 were replaced with the single statement add $v0,$v0,$a1. Would the resulting fragment still work? Explain. (5 pts) It would not work. If you saved the value returned from the first call to fib in a scratch register (like $a1 in this case). Subsequent, calls by non-leaf children would overwrite it, thus making it unavailable upon return. Comp 411 - Fall 2006-1 - Problem Set #4
c) Rewrite an iterative version of the fib() function that complies with the procedure linkage convention discussed in Lecture 7 and based on the Fibonacci code fragment given in Lecture 6. (20 pts) int fib(int n) { int a, b, t; if (n < 2) return n; else { a = 0; b = 1; n -= 1; while (n!= 0) { t = a; a = b; b += t; n -= 1 return b; fib: slti $t0,$a0,2 beq $t0,$0,else add $v0,$0,$a0 beq $0,$0,rtn else: addi $t0,$0,0 addi $t1,$0,1 beq $0,$0,test while: add $t2,$t0,$0 add $t0,$t1,$0 add $t1,$t1,$t2 test: addi $a0,$a0,-1 bne $a0,$0,while add $v0,$0,$t1 rtn: j $ra d) Discuss the differences between your iterative fib() implementation and the given recursive one. Which is faster? Shorter? Uses less memory? Easier to understand? (5 pts) The iterative version is a leaf routine, and all variables can be allocated in registers, thus, no stack space is needed and it requires less memory. The assembly language implementation is also shorter, and faster since a Fibonacci number is only computed once, whereas the same Fibonacci numbers are computed several times in the recursive version. For example: fib(5) = fib(4)+fib(3) fib(5) = (fib(3)+fib(2))+(fib(2)+fib(1)) fib(5) = ((fib(2)+fib(1))+(fib(1)+fib(0))+((fib(1)+fib(0))+fib(1)) fib(5) = ((((fib(1)+fib(0))+ fib(1))+(fib(1)+fib(0)) + ((fib(1)+fib(0))+fib(1)) Note that fib(3) is computed twice, and fib(2) is computed 3 times. This redundancy only gets worse as n grows (it grows proportional to n 2 ). Therefore, the iterative version is faster than the recursive one. Perhaps the iterative version is slightly easier to understand. There is some subtly in the iterative code for example, the need for the t variable to manage the updating of the n-1 and n-2 Fibonacci numbers. Comp 411 - Fall 2006-2 - Problem Set #4
Suppose that at some point during the execution of the given recursive fib() function the computer is interrupted and the stack is examined and found to contain the following: Memory Address Memory Contents 0x7fffefe0 0x0040007c 0x7fffefdc 0x00000007 0x7fffefd8 0x5f36c89e 0x7fffefd4 0x00400048 0x7fffefd0 0x00000006 0x7fffefcc 0x8d197d50 0x7fffefc8 0x00400048 0x7fffefc4 0x00000005 0x7fffefc0 0xb89f3675 0x7fffefbc 0x00400048 Memory Address Memory Contents 0x7fffefb8 0x00000004 0x7fffefb4 0x0941c475 0x7fffefb0 0x00400048 0x7fffefac 0x00000003 0x7fffefa8 0xeb3ee605 0x7fffefa4 0x00400048 0x7fffefa0 0x00000002 0x7fffef9c 0x00000001 0x7fffef98 0x00400058 0x7fffef94 0x00000001 $sp 0x7fffef90 0x5c4ee709 If you use the MIPS simulator/assembler, SPIM, as an aid in answering the following questions (which might be a good idea, though it is not necessary), you need to be aware of the following caveat. SPIM assumes that all of memory, outside the loaded.text and.data segments is filled with zeros. In reality, this is usually not the case. Upon power up, memory locations are filled with apparently random values, and over the lifetime of a program the uninitialized values on the stack reflect the activation records of previously called procedures. Therefore, you need to consider that some of the values shown in the above stack dump may reflect uninitialized memory locations. e) At what memory address can the function fib() be found? (5 pts) The trick here is to first find some stack frame for an instance of fib(). Each stack frame is composed of three words, the first word being the return address and the second word being the argument passed in. Notice that successive calls to fib() are with arguments one or two less than the caller s. If we look into this stack dump, we can see a 3-word pattern starting at location 0x7fffefe0. We can surmise that the contents of 0x7fffefe0 are the return address of the first self-call of fib (with argument n-1). From this we can figure out that the function fib() must be located at 0x00400024 (0x00400048 4*9). f) What argument (value of n) was passed to the originating call? (5 pts) The argument of the original call was 7. One indication that this is the initiating call is that the return address 0x0040007c is outside of the fib() routine. g) What is the label of the last executed instruction before the machine was interrupted? (5 pts) This is a tricky question. If you examine the stack carefully, it appears that most stack frames do not have their 3 rd element initialized. By examining the code, one can see that immediately upon return from the first self-call (fib(n-1)), the returned value ($v0) is stored on the stack, as is evident from the 1 stored in stack location 0x7fffef9c. You can also see that the return address of the next call is different from those previous, which indicates that the second call to fib(n-2) has already taken place. This call would be the second call of a callee whose argument was 2 (from stack location 0x7fffefa0), thus, fib is called with 2-2 = 0, and this 0 has not yet been stored onto the stack. Thus, the stack dump must have occurred in a call with fib(0) after the instruction with label L2. Comp 411 - Fall 2006-3 - Problem Set #4
h) What would have been the lowest memory location referenced by the stack pointer during this particular invocation? (5 pts) The deepest stack recursion is determined argument. If fib is called with n, then a stack frame will be allocated for fib calls with arguments n-1 and n-2. Fib is called again until n is either 1 or 0. The second call to fib (n-2) reuses the same memory used by the first call (n-1). Thus, the depth of the stack is equal to 3 times the argument, in this case 21 locations. So the lowest memory location allocated on the stack in this location is 0x7fffef90. However, in the final call of fib, the third stack entry is never used, thus the lowest memory location referenced is 0x7fffef94. Problem 2. Growing Up Fast Consider the following simple recursive C function: int ack(m,n) { if (m == 0) return n+1; if (n == 0) return ack(m-1,1); else return ack(m-1, ack(m, n-1)); The function ack(m,n) is defined for all non-negative values of m and n. a) Write a MIPS assembly code version of ack() using the procedure calling conventions discussed in lecture. (Note: This function trickier than any you have seen before, since one of the arguments is an expression (the result of a function call). Recall from lecture that in the C language, all expressions that are passed as arguments to a function are evaluated by the Caller, before invoking the Callee procedure). (20 pts) ack: addu $sp,$sp,-8 sw $ra,4($sp) # return address sw $a0,0($sp) # same m bne $a0,$0,testn add $v0,$a1,1 beq $0,$0,return # return n+1 testn: bne $a1,$0,else addi $a0,$a0,-1 addi $a1,$0,1 beq $0,$0,return # return ack(m-1,1) else: subu $a1,$a1,1 move $a1,$v0 lw $a0,0($sp) addi $a0,$a0,-1 return: lw $ra,4($sp) addu $sp,$sp,8 j $ra Comp 411 - Fall 2006-4 - Problem Set #4
b) Using your implementation of ack() and SPIM to compute the returned values for the following function calls: ack(0,0), ack(1,3), ack(3,1), ack(2,4). Which of these calls makes the most recursive calls to ack()? (15 pts) ack(0,0) = 1, ack(1,3) = 5, ack(3,1) = 13, ack(2,4) = 11 ack(3,1) makes 106 calls to ack( ). c) The value for ack(4,1) is 65533, but your MIPS implementation probably can t compute it. Can you explain why? Consider the following, the value of ack(4,2) is greater than the number of atoms in the known universe. (10 pts) We can t compute ack(4,1) because the stack would overflow, or else run into the program code. Comp 411 - Fall 2006-5 - Problem Set #4