5.1.3 子程序
当设计一个执行运算的机器时,我们优先设计能被运算的不同的部分共享的组件,而不是单独的组件。
考虑这种情况:一个机器有两个求最大公约数的运算,一个是找到寄存器A,B的内容的最大公约数,
另一个是找到寄存器C,D的内容的最大公约数。开始时我们可能设计出一个原生的求最大公约数的操作,
然后扩展为用最原生的操作来生成 求最大公约数的两个实例。在图5.7中,仅仅显示出了机器的数据路径的
求最大公约数的部分,没有显示出它们如何连接到机器的其它部分。图中也显示了机器的控制序列的
相应部分。
gcd-1
(test (op =) (reg b) (const 0))
(branch (label after-gcd-1))
(assign t (op rem) (reg a) (reg b))
(assign a (reg b))
(assign b (reg t))
(goto (label gcd-1))
after-gcd-1
.
.
gcd-2
(test (op =) (reg d) (const 0))
(branch (label after-gcd-2))
(assign s (op rem) (reg c) (reg d))
(assign c (reg d))
(assign d (reg s))
(goto (label gcd-2))
after-gcd-2
图5.7 对一个GCD机器中有两个GCD计算的数据路径和控制器序列的部分
这个机器有两个求余数的操作盒子和两个测试相等性的盒子。如果
重复的组件是复杂的,正如余数盒子,在构建时这将不是经济的方式。
为了GCD计算,通过使用相同的组件,我们能够避免重复的数据路径组件,
这么做不影响更大规模的机器的计算的其它部分。如果在寄存器a,b中的值
在控制器取gcd-2的时候不再需要了(或者是这些值能被移动到其它的
寄存器中保存起来),我们能修改机器让它来使用寄存器a,b而不是寄存器c,d
在计算第二个GCD像第一个一样。如果我们这么做,我们得到了控制器序列如图5.8
我们已经移除了重复的数据路径组件,但是现在控制器有两个GCD序列它们只有
入口点的标签不同。如果通过对一个单独的序列进行分支,也就是一个GCD的子程序
在它的结尾处我们把分支回到主指令序列的正确的位置,以这种方法来代替这两个序列
就更好了。我们完成这个任务如下:在分支到GCD之前,我们把一个特定的值(例如0或者1)
放入一个特定的寄存器continue中。在GCD子程序的结尾处,我们返回到after-gcd-1 或者
after-gcd-2 ,这依赖于寄存器continue的值。图5.9显示了这个结果的序列的相关的部分,
它仅包括了gcd指令的一个拷贝。
gcd-1
(test (op =) (reg b) (const 0))
(branch (label after-gcd-1))
(assign t (op rem) (reg a) (reg b))
(assign a (reg b))
(assign b (reg t))
(goto (label gcd-1))
after-gcd-1
.
.
gcd-2
(test (op =) (reg b) (const 0))
(branch (label after-gcd-2))
(assign t (op rem) (reg a) (reg b))
(assign a (reg b))
(assign b (reg t))
(goto (label gcd-2))
after-gcd-2
图5.8 对于有两个不同的GCD计算的并且使用相同的数据路径组件的机器而言
控制器序列的部分
gcd
(test (op =) (reg b) (const 0))
(branch (label gcd-done))
(assign t (op rem) (reg a) (reg b))
(assign a (reg b))
(assign b (reg t))
(goto (label gcd))
gcd-done
(test (op =) (reg continue) (const 0))
(branch (label after-gcd-1))
(goto (label after-gcd-2))
....
(assign continue (const 0))
(goto (label gcd))
after-gcd-1
....
(assign continue (const 1))
(goto (label gcd))
after-gcd-2
图5.9 使用一个continue寄存器来避免图5.8中的重复的控制器序列
gcd
(test (op =) (reg b) (const 0))
(branch (label gcd-done))
(assign t (op rem) (reg a) (reg b))
(assign a (reg b))
(assign b (reg t))
(goto (label gcd))
gcd-done
(goto (reg contiue))
....
(assign continue (label after-gcd-1))
(goto (label gcd))
after-gcd-1
....
(assign continue (label after-gcd-2))
(goto (label gcd))
after-gcd-2
图5.10 把标签的值赋给continue寄存器,简化并且生成了图5.9的策略。
在处理小问题时,这是合理的方法,但是,如果在控制器序列中有许多的GCD计算的实例,这就是
有点难了。在GCD的子程序之后,我们要确定继续执行哪的指令,我们将需要测试在数据路径
和在控制器中使用GCD的所有的地方的分支指令。为了实现子程序的一个更强有力的方法是让继续的寄存器
在控制器的序列中保存入口点的标签,当子程序完成后,就执行寄存器中指定的地方。实现这个策略需要在数据路径和寄存器机器中的控制器之间有一个新型的连接:这必须是一种方式在控制器序列中为一个寄存器
赋值一个标签,并且这个标签值能被从寄存器中取出来,并且作为指定的入口点来继续执行。
为了反映这种能力,我们将扩展寄存器机器的语言的赋值指令,让它可以把控制器序列中的标签的值赋给一个寄存器。我们也扩展了goto指令,允许执行的入口点被描述为一个寄存器的值而不仅仅是一个常数的标签所描述的入口点。使用这个新的组装,我们能中止gcd子程序,以保存在继续的寄存器中的位置的分支。
这导致了图5.10中显示的控制器的序列。
有多于一个子程序的机器,能使用多个继续的寄存器(例如,gcd-continue,factorial-continue)或者
我们有多个子程序共享一个继续的寄存器。共享是经济的,但是我们必须很小心,如果我们有一个子程序sub1,它调用了另一个子程序sub2. 为了调用sub2,而设置continue之前,如果我们没有在sub1中把continue的值保存到其它的寄存器中,sub1将在它自己完成时不知道去哪里。在下一部分中开发的机制处理了递归,也提供了
嵌套的子程序调用这个问题的更好的解决方案。