Mon, 6th Nov 2017, 02:49 Some stuff on Recursion
My students, I have written most of a section on Recursion which will be part of the 3rd edition of the textbook. Given last Wednesday's lecture, I hope you will find this a useful addition to the lecture. In the interest of getting this to you quickly, I will admit that I have not yet had a chance to check to see if the code shown below is bug free. If you find an error in it, please let me or one of my TAs know so I can let the rest of the class know. See you later today! Yale Patt ===================== 8.3.6. Recursion, a powerful technique that requires a stack Recursion is a mechanism for expressing a function in terms of itself. Some have referred to it as picking oneself up by one's bootstraps, since at first blush, it looks like magic -- which, of course, it isn't. The simplest example to illustrate recursion is the function: factorial. The equation n! = n * (n-1)! says it all. We are expressing factorial in terms of factorial. How we can write a program to do this we will see momentarily. For now, it is important to understand that recursion is a powerful technique for expressing the structure of an algorithm efficiently. Where useful, it can help the programmer by removing a lot of detail that the programmer would otherwise have to keep track of. When used capriciously, it can cause the resulting program to run much more slowly than is necessary. In this chapter, we will show three examples, the first two use recursion when it would be better not to; the third example uses recursion judiciously. To implement recursion, we will need a partner we have just recently discussed: a stack. We start our discussion of recursion with two bad uses of recursion that are often used in the classroom to explain the concept even though they incur very costly penalties in execution time. 8.3.6.1. Bad example Number 1: Factorial. As stated above, n! = n * (n-1)! Assume the subroutine FACT (Factorial) is supplied with a value n in R0, and returns with the value n! in R0. The basic structure of a subroutine to do this is shown below: FACT ST R1, SAVE_R1; R1 is needed in the subroutine ADD R1,R0,#-1 ; Test if R0=1 BRz DONE ; If R0=1, R0=(1)! ADD R1,R0,#0 ; Save n, to be used after we compute (n-1)! ADD R0,R1, #-1 ; R0 is set to n-1, preparatory to calling FACT B JSR FACT ; On RET, R0 will contain (n-1)! MUL R0,R0,R1 ; Now R0 should contain n! DONE LD R1, SAVE_R1; Restore R1 RET SAVE_R1 .BLKW 1 The subroutine first tests to see if n=1. If so, we are essentially done, since (1)! =1. Note we did save R1 before our test since the calling program has every right to expect that after returning, R1 has not changed as a result of executing the subroutine FACT. So, if n=1, we branch to DONE where we restore R1, and then RET to the calling program. We must emphasize that every recursive subroutine must have such an initial test to see if we should execute the recursive JSR FACT. Without this test, the subroutine would call itself an infinite number of times! Clearly, that can not be correct. The answer is to provide a test before the recursive JSR instruction. In the current case, the test is whether R0 contains the value 1. If yes, we do not execute a JSR FACT, and instead set R0 to 1 (since 1! = 1) and RET. If n is not equal 1, we save n in R1, load R0 with n-1 and JSR FACT. When FACT returns with (n-1)! in R0, we multiply it by n (which was saved in R1), producing n!, which we load into R0, restore R1 to the value expected by the calling program, and RET. We are assuming that our ISA has a MUL instruction. Since the LC-3 does not have a MUL instruction, this will require another subroutine call, but we are ignoring that here in order to focus on the recursion. We call the subroutine recursive because inside the FACT subroutine is an instruction JSR FACT. Unfortunately, the code we have written will not work. To see why it will not work, Figure New.11 shows the flow of instruction execution. The main program calls the subroutine with a JSR instruction at address A. This causes the code labeled #1 to excute. At address B, the subroutine FACT calls itself with the instruction JSR FACT. This causes the code labeled #2 to execute, and so forth. Note that when the main program executes the instruction JSR FACT, the return linkage A+1 is saved in R7. In the block of code labeled #1, the instruction at address B (JSR FACT) stores its return linkage B+1 in R7, destroying A+1, so there is no way to get back to the main program. Bad! We can solve this problem by pushing the address A+1 onto a stack before executing JSR FACT at address B. After we subsequently return to address B+1, we can then pop the stack, and load the address A+1 into R7 before we execute the instruction RET back to the main program. Furthermore, note that the instruction ADD R1,R0,#0 in (1) loads the value n into R1, and in (2), the instruction ADD R1,R0,#0 loads the value n-1 into R1, thereby wiping out the value n that had been put there by the code in #1. Thus, when the instruction flow gets back to (1) where the value n is needed by the instruction MUL R0,R0,R1, it is no longer there. It was previously wiped out. Bad! We can solve this problem with a stack also. That is, instead of moving the value n to R1 before loading n-1 into R0, we push n onto the stack, and then pop it when we need it after returning from the subroutine with (n-1)! in R0. Finally, we note that the first instruction in our subroutine saves R1 in SAVE_R1 and the last instruction before the RET restores it to R1. We do this so that from the standpoint of the calling program, the value in R1 before the subroutine is the same as the value in R1 after the subroutine, even though the subroutine used R1 in performing its job. However, since our subroutine is recursive, when FACT is called by the JSR instruction at address B, R1 does not contain the value it had in the main program, but instead it has the value last stored in R1 by the ADD R1,R0,#0 instruction. Thus after the JSR FACT instruction is executed, the first instruction of the recursively called subroutine FACT will save that value, wiping out all information about the value in R1 when the main program called FACT. We can solve this problem with a stack also. We simply replace the ST R1,SAVE_R1 with a push and LD R1,SAVE_R1 with a pop. If we make these changes (and if the LC-3 had a MUL opcode), the recursive subroutine would work as we would like it to. The resulting subroutine is as shown below (with almost all instructions explained via comments): FACT ST R1,SAVE_R1 ; We temporarily save R1 in SAVE_R1. ADD R1,R0,#-1 ; As before, we test for the case n=1. BRz NO_RECURSE ADD R1,R0,#0 ; We move n to R1 to free R0 for PUSH subroutine ADD R0,R7,#0 ; We move the return linkage to R0, ... JSR PUSH ; and push it onto the stack LD R0,SAVE_R! ; We move temporarily saved R1 into R0, ... JSR PUSH ; and push it onto the stack ADD R0,R1,#0 ; We move n from R1 to R0, ... JSR PUSH ; and push it onto the stack ADD R0,R1,#-1 ; R0 = n-1 JSR FACT ; The subroutine FACT will return R0 = (n-1)! ADD R1,R0,#0 ; moves (n-1)! to R1 JSR POP ; pops n into R0 MUL R1,R1,R0 ; R1 now contains n! JSR POP ; pops the calling program's R1, ... ST R0,SAVE_R1 ; and stores it in SAVE_R1 JSR POP ; pops the return linkage, ... ADD R7,R0,#0 ; and stores it in R7 ADD R0,R1,#0 ; moves n! (the result of the subroutine) to R0 NO_RECURSE LD R1,SAVE_R1 RET SAVE_R1 .BLKW 1 As we said, Figure New-11 shows the execution flow of the recursive subroutine call. The main program calls FACT with R0=n. The code in (1) executes, with JSR FACT being called with R0 = n-1. At this point, the stack contains the three entries pushed, as shown in Figure New-12a. When the JSR FACT instruction in (3) executes, with R0 = n-3, the contents of the stack contains the nine entries as shown in Figure New-12b. The obvious question you should ask at this point is, "Why is this such a bad use of recursion, particularly when its representation n! = n * (n-1)! is so elegant?" To answer this question, we first note how many instructions are executed and how much time is wasted pushing and popping elements off the stack. AND, the second question you should ask is "Is there a better way to compute n!? Consider this alternative: FACT ST R1,SAVE_R1 ADD R1,R0,#0 ADD R0,R0, #-1 AGAIN MUL R1,R1,R0 ADD R0,R0,#-1 BRnp AGAIN ADD R0,R1,#0 LD R1,SAVE_R1 RET SAVE_R1 .BLKW 1 8.3.6.2. Fibonacci, an even worse example >> I will add this code when I get a chance<< 8.3.6.3. The maze, a good example. >>and this one also<<