increment
(assign sum (op +) (reg
sum) (const 1))
(goto (reg continue))
Note new goto form: indirect jump.
-- look up a label in register
-- then jump to it
How do you use the subroutine? For example, if you'd written the
instruction
(inc sum)
with the expectation that it would increment the contents of the
sum register, but then discovered that there was no inc instruction,
what would you write instead?
(assign continue (label after-call))
(goto (label increment))
after-call
Key ideas:
1. need to put number to be incremented in sum register
2. need to put return address in register continue
Sample use, increments twice from 0:
(assign sum (const 0))
(assign continue (label
after-call-1))
(goto (label increment))
after-call-1
(assign continue (label
after-call-2))
(goto (label increment))
after-call-2
What did we need to know about the subroutine?
-- where args and return values go
-- where return label goes
A more subtle point
-- subroutine may use registers that caller has values in
-- no notion of "local variables"!
-- so must also know which registers may be damaged
-- will see can avoid writing registers by using stack
Consider a subroutine for multiplication:
mul
(assign result (const 0))
start
(test (op =) (reg y) (const
0))
(branch (reg continue))
(assign result (op +) (reg
result) (reg x))
(assign y (op -) (reg y)
(const 1))
(goto (label start))
This does something potentially bad:
-- changes the value of y
So caller must save the register y if it plans to use
the value again.
By the way, we've been happily picking names for registers as if there
was an unbounded
number of them. In practice, there's a small finite number, so registers
have
to be reused. A compiler takes symbolic names (such as continue and
sum) and
maps them to physical registers (R0, R1, ...).
Extend machine with stack. Unlike registers, provides (almost) unbounded
memory.
Stack is LIFO: last in, first out.
New instructions are:
(save register)
take value in register and push it on the stack
(restore register)
pop value off stack and put in register
Can put numbers and labels on the stack. We'll do both:
-- numbers to save values of registers inside subroutines
-- labels to remember where to go back to after a subroutine call.
Exercises:
What are the values in each register after ...?
(assign x (const 0))
(assign y (const 1))
(save x)
(save y)
(restore x)
(restore y)
Now consider this:
(assign x (const 0))
(assign y (const 1))
(save x)
(save y)
(restore y)
(restore x)
Note that
(save R)
(restore R)
is a no-op and can be optimized away.
We can use the stack to solve our multiplication problem. We just save
the resister
y before the call and restore it after:
Instead of
(assign result (op *) (reg x) (reg y))
we would have
(save y)
(assign continue (label
after-call))
(goto (label multiply))
after-call
(restore y)
(define (fact n)
(if (= n 1) 1
(* n (fact (- n 1)))))
(fact 3)
(* 3 (fact 2))
(* 3 (* 2 (fact 1)))
(* 3 (* 2 1))
(* 3 2)
6
Where do all the pending computations get stored?
-- on the stack
Let's study the translation of factorial: it's about the simplest recursive
procedure
(but its machine language translation is rather verbose!):
(controller
(assign continue (label
halt))
fact
(test (op =) (reg n) (const
1))
(branch (label b-case))
(save continue)
(save n)
(assign n (op -) (reg n)
(const 1))
(assign continue (label
r-done))
(goto (label fact))
r-done
(restore n)
(restore continue)
(assign val (op *) (reg
n) (reg val))
(goto (reg continue))
b-case
(assign val (const 1))
(goto (reg continue))
halt)
We drew a picture to explain what was going on.
(assign sum (const 0))
(save continue)
(assign continue (label
after-call))
(goto (label increment))
after-call
(restore continue)
(goto continue)
...
increment
(assign sum (op +) (reg
sum) (const 1))
(goto (reg continue))
What's going on with the continue register?
-- caller saves it on the stack and stores label after-call in it
-- increment returns to label in register continue
-- caller just restores register, and jumps to its label
Result
-- call to increment is RECURSIVE
If we translated like this, an iterative procedure such as
(define (f x)
(if (= x 0) 1 (f (- x 1))))
would have recursive behaviour!
To eliminate this problem, we can optimize the code:
-- have increment jump back to the caller's caller
-- replace
(assign sum (const 0))
(save continue)
(assign continue (label
after-call))
(goto (label increment))
after-call
(restore continue)
(goto continue)
by
(assign sum (const 0))
(goto (label increment))
Change the code for fact to do this
Do as few additional saves and restores as possible
You are allowed to change the contract for fact if you want to
Hint: use the tail-call optimization