Forth 系统实现 |
作者 Brad Rodriguez |
| 编译 赵宇 张文翠 |
第一部分 Forth 内核的设计决策
前言
每一个进入 Forth 圈里的人都说或者听说“把 Forth 移植到一个新 CPU 上是一件易如反掌的事情”。不过,就像其它许多“易如反掌”的事情一样,却没有多少书面的资料告诉我们如何去做!所以,当 Bill Kibler 建议这个论文题目时,我决定打破 Forth 编写者只说不练的传统,给出一个白纸黑字的 Forth 实现 , 包括为 MC6809 、 Intel 8051 和 Z80 实现的 Forth 系统。
在整个文档中,我准备为 MC6809 、 Intel 8051 和 Zilog Z80 实现 Forth 系统。我会用 MC6809 来解释一个简单的和传统的 Forth 模型,此外,我还将公布一个 MC6809 汇编器,把 6809 Forth 用于未来的 TCJ 计划,把 8051 作为一个大学项目,其中也解释了一些非常不同的决策。 Z80 Forth 是为所有的 TCJ CP/M 读者和许多 TRS-80 的老朋友而编写的。
有效的硬件
首先,我们必须选择一个 CPU 。不过,我不想陷入“Forth 运行在这种 CPU 上比运行在那种 CPU 上更有效”的争论中,因为 CPU 的选择通常还需要考虑其它因素,并且这篇论文的目标之一就是想说明如何把 Forth 搬到任何一个 CPU 上。
通常, 16 位 Forth 内核需要 8K 字节的程序空间。对于一个能够真正用来编译 Forth 语言应用的完整内核来说,应该至少有 1K 字节的 RAM 。如果想使用 Forth 的磁盘存储器块管理功能,还应该再增加 3K 字节以上的 RAM 用于缓冲区。对于 32 位系统,这些数值都需要加倍。
以上这些是一个 Forth 内核能够运行的最小要求。为了在硬件上运行应用程序,你还得按实际需要另外增加 PROM 和 RAM 的大小。
使用 16 位还是 32 位系统
实际的系统并不要求 Forth 的字长度与 CPU 的字长度一致。最小的、实际可用的 Forth 系统都使用 16 位模型,也就是说使用 16 位的整数和 16 位的地址。 Forth 的术语把这种尺寸称为单元(CELL)而不是我们常说的“字”,因为“字”在 Forth 中是指一个 Forth 定义(可以简单地理解成其它高级语言的子程序名)。
所有的 8 位 CPU 几乎都不加改变地支持 16 位的 Forth ,为此,要求编码进行双字节算术运算,尽管某些 8 位 CPU 也能够直接支持其中的一些操作。
有一些技术可以在一个16位的机器上写出 32 位的 Forth ,但是 16 位的 CPU 通常运行 16 位的 Forth ,虽然我们也看到 32 位的 Forth 可以运行在 Intel 8086/8088 上。
32 位的 CPU 通常运行 32 位的 Forth 。实际应用中,一个更小的模型几乎不能节省代码空间和处理器时间。但我也见到过为 MC68000 编写的 16 位 Forth 。这个系统的代码长度缩小了 2 倍,因为高级 Forth 定义变成了 16 位的地址串而不再使用 32 位的地址串。不过大多数的 MC68000 系统都有很多 RAM ,好象没有必要进行这样的努力。
本文描述的所有例子都是运行在 8 位 CPU 上的 16 位 Forth 系统。
串线编码技术
“串线编码”是 Forth 的标志。一个 Forth “串线”就是被执行的子程序地址的列表。你可以把它们想象成一连串省略了 CALL 指令的子程序调用表。长期以来,人们发明了多种串线形式,为了作出选择,你必须理解所有这些串线形式是如何工作的,以及它们各自的优缺点。
间接串线编码( ITC )
间接串线编码(ITC)技术是一种经典的 Forth 串线编码技术,最早出现在 FIG-Forth 和 F83 系统中,并且在许多关于 Forth 的书中都有描述。后来的串线方式都是直接串线编码方式的“发展”,所以,你需要先理解这个技术。
让我们看一个 Forth 字 SQUARE 的定义 :
: SQUARE DUP * ;
在一个典型的 ITC Forth 中,该定义在存储器中的情况如图 1 所示(首部将在以后讨论,它保存编译信息,但并不在串线中访问)

图 1 ITC Forth 定义的存储器
假设在执行某个 Forth 字的时候遇到了字 SQUARE , Forth 的解释指针 IP 将指向存储器的一个单元,其中包含字 SQUARE 的地址,当然更严格地应该说,这个单元包含着 SQUARE 的“代码域地址”。解释器读出这个地址并用这个地址读出 SQUARE 的代码域内容。它还是一个地址 -- 这个地址是一个机器语言子程序,由这个子程序来执行定义 SQUARE 。
我们可以把上面的描述通过伪码表示如下:
(IP)-> W 读取 IP 指向的存储器内容到 W 寄存器, W 现在有代码域的地址;
IP+2->IP 增量 IP, 它就像一个程序计数器,而且假设串线中的地址是 2 个字节长;
(W) -> X 读取由 W 指向的存储器内容到 X 寄存器, X 现在指向机器码地址;
JP (X) 跳到 X 寄存器指向的地址执行;
这里解释了一个重要的、但却很少有人说明的原理:进入的 Forth 字的当前地址保存在 W 寄存器中。 CODE 字不需要这个信息,但是其它类型的 Forth 字确实需要其中的信息。
如果 SQUARE 是由机器代码写成的,事情也就结束了:这些机器代码被执行,然后跳回到 Forth 解释器 -- 由于 IP 已经增量,它将指向下一个将被执行的字。所以 Forth 解释器通常被称为 NEXT 。
可是, SQUARE 是一个高级的“冒号”定义 -- 它保持一个“串线”,或者说一个地址列表。为了执行这个定义, Forth 解释器必须在一个新的位置上重新启动,这个位置就是 SQUARE 的参数域。当然,解释器必须保存旧的位置,以便 SQUARE 结束之后能够恢复“另一个” Forth 字。这实际上与一个子程序调用没有任何区别!
SQUARE 机器语言的动作就是简单地把旧的 IP 值保存到堆栈上,并把 IP 指向新的位置,执行解释器,当 SQUARE 完成后弹出恢复 IP 。(正如你看到的, IP 就是 Forth 高级定义的“程序计数器”),这个过程在不同的 Forth 版本中可能被称为 DOCOLON 或者 ENTER :
PUSH IP 压入“返回地址栈” ;
W+2 -> IP W 已经指向代码域,所以 W+2 就是定义体的地址! ( 假设是 2 字节的地址,不同的 Forth 可能不同 )
JUMP NEXT 到解释器 ( “ NEXT ” )
这样的一段代码被用于所有的高级(串线) Forth 定义!于是我们回答了两个问题:
为什么在 Forth 定义中用一个指针指向代码段,而不是把代码段本身直接嵌入到定义中。因为如果有数以百计的定义,就可以节省大量的空间;
为什么这种方式被称为“间接串线编码”;
“从子程序返回”动作由字 EXIT 完成,它被 Forth 的分号“;”编译进定义中(有些 Forth 系统使用 ;S 替代 EXIT)。 EXIT 执行下列的机器语言:
POP IP 从“返回地址栈”弹出指针
JUMP interpreter 跳转到解释器
注意 ITC 的特点:每个 Forth 字都有一个单元的代码域,冒号定义给定义中的每一个字编译一个单元。 Forth 解释器为了执行机器代码,必须实际执行两次间接才能取得下一个机器码的地址(首先通过 IP ,然后通过 W)。
ITC 既不是代码尺寸最小的、也不是执行速度最快的串线技术。它可能只是最简单的技术,尽管下面讨论的另一种技术DTC 实际上也不是特别复杂。那么为什么有这么多的 Forth 系统都使用间接串线技术呢?主要是由于以前作为原始模型的 Forth 系统都使用间接串线技术,而现在, DTC 技术却用得最多。
那么什么时候应该使用 ITC 技术呢?很明显, ITC 形式能够产生最纯净和最一致的定义:其中只有一种类型,这种类型就是地址。如果你正好就有这样的需要,那 ITC 技术就是适合的。如果你的代码关注定义的内部, ITC 技术的简单性和单一性还能够增加可移植性。
此外, ITC 是经典的 Forth 模型,它可以非常好地用于教学。
最后,在某些缺少子程序调用指令的早期 CPU 上 -- 比如 1802 -- ITC 常常比 DTC 更有效。
直接串线编码( DTC )
直接串线编码(DTC)技术与 ITC 技术的差别只有一点:不像 ITC 在代码域中包含机器码的地址, DTC 的代码域中包含有实际的机器代码本身。
注意,我并不是说在每个冒号定义中都包含全部的 ENTER 代码。我的意思是:在“高级” Forth 字中,如图 2 所示,代码字段有一个子程序调用指令。例如,冒号定义中将包含一个对 ENTER 子程序的调用。

图 2 DTC Forth 定义的存储
直接串线的 NEXT 伪代码非常简单:
(IP) -> W 取 IP 指针指向的存储器内容到 W 寄存器中
IP+2 -> IP 增量 IP ( 假设 2 字节的地址 )
JP (W) 跳转到 W 寄存器指向的地址执行
DTC 的收益就是速度:解释程序现在只需要执行一次间接。在 Z80 上,这实际是把 NEXT 子程序 --Forth 内核中最常用的代码段 -- 从 11 个指令减少到了 7 个指令。
DTC 的成本是空间:在一个 Z80 Forth 中,每个高级定义都将增加一个字节的长度,因为 ITC 中 2 个字节的地址现在被 3 个字节的 CALL 调用指令所取代。当然这个结论也不是广泛适用的,在 32 位的 MC68000 Forth 中,可以用 4 字节的 BSR 指令代替 4 字节的地址,其中没有任何差异。而在 Zilog 的 SUPER8 中,有一个直接用于 Forth DTC 的指令,它用一个字节的 ENTER 指令代替 2 字节的地址,使得在 SUPER8 上, DTC Forth 比 ITC Forth 的代码还要小。
当然 DTC 的 CODE 定义也缩短了 2 个字节,因为它们不再需要指针。
我一直以为 DTC Forth 的高级定义字必须在代码域中使用子程序调用指令, Frank Sergeant的 Pygmy Forth [SER90] 提出可以使用更简单的跳转指令,这样更容易实现,通常也更快。
Guy Kelly 对 IBM PC 上实现的 Forth 系统进行了很好的总结,这也是我对所有 Forth 编写者的建议。
在他研究的 19 个 Forth 实现中,有 10 个使用了 DTC 技术, 7 个使用了 ITC 技术, 2 个使用了子程序串线技术(这种技术我们将在下面讨论)。所以,我认为所有新实现的 Forth 内核都应该使用直接串线技术,而不要再使用间接串线技术了。
跳转到 NEXT 还是对 NEXT 使用内嵌编码?
Forth 的内层解释器 NEXT 是一个用于所有 CODE 定义的通用子程序。你可以编写一个子程序,然后让所有的 CODE 字跳转到这个子程序上执行(注意:跳转到 NEXT 而不必通过子程序调用到 NEXT )。
然而, NEXT 的速度对于整个 Forth 系统的速度来说是至关重要的,从这个角度考虑, NEXT 最好是内嵌代码,于是 NEXT 也可以被定义成一个汇编的宏。
这就是一个常见的速度 / 空间折衷问题:内嵌的 NEXT 总是更快,但也总是更大。全部增加的尺寸数量是内嵌扩展需要的字节数乘以系统中 CODE 字的数量。当然有时也根本不需要考虑折衷:在 MC6809 中,内嵌的 NEXT 总是比一个 JUMP 指令还要短!
子程序串线( STC )
一个高级的 Forth 定义字只不过是“要执行的子程序的列表”,并不一定要通过解释才能实现它们,你也可以通过简单地调用一系列子程序而得到同样的效果:
SQUARE:
CALL DUP
CALL * ; 或者是一个合适的名字,因为有些汇编器不支持把 * 作为子程序名
RET
图 3 为汇编程序员解释了 Forth 的 STC 串线技术。 [KOG82].

图 3 DTC Forth 定义的存储
STC 有一个统一的表示方式,冒号定义和 CODE 字没有区别,“定义字”(这是 Forth 的专用术语,像 VARIABLE 、 CONSTANT 这样一些可以用来定义新字的字被称为定义字)像 DTC一样处理 -- 代码域用一个跳转或者调用指令转到其它地方的机器码。
STC 的一个主要缺点是:子程序调用指令通常比简单的地址列表址大。比如在 Z80 上,冒号定义的尺寸将增大 50% -- 而你的应用中大部分都是冒号定义。相比在32位的 MC68000 上,如果使用 4 字节的 BSR 代替 4 字节地址,代码尺寸没有任何增加,不过,如果你的代码超过了64K ,一些地址就必须用 6 字节的 JSR 代替。
子程序串线可能比直接串线更快。在 STC 中节省了解释器执行的时间,但必须花费 Forth 字用于返回的 PUSH 、 POP 时间。而在 DTC Forth 中,只有高级定义才引起返回栈动作,在 MC6809 或者 Zilog SUPER8 中, DTC 比 STC 更快。
STC 还有一个优点:它不需要 IP 寄存器。有些处理器 -- 像 Intel8051 -- 缺少地址寄存器,没有虚拟机 IP 寄存器可以真正地简化内核并提高速度。
STC 的内嵌扩展、优化、直接编译
在一些古老的 8 位 CPU 上,几乎每个 Forth 原语都需要用几个机器指令才能实现,但是在更强大的 CPU 上,有时 Forth 原语只需要一个机器指令。例如,在一个 32 位的 MC68000 上, DROP 可以简化为:
ADDQ #4,An 这里 An 是 Forth 的 PSP 参数栈寄存器
在一个子程序串线的 Forth 中,冒号定义中使用 DROP 将产生这样的序列:
BSR ...
BSR DROP ...
DROP:
ADDQ #4,An
BSR ...
RTS
ADDQ 本来是一个 2 字节指令,我们为什么要写一个对这个 2 字节指令的 4 字节子程序调用呢?在这种情况下,不论有多少个 DROP ,通过子程序调用都不会产生任何的节省。而如果把 ADDQ 直接编码到 BSR 流中,产生的代码都会更小,运行得更快。有些 Forth 编译程序已经实现了这样的 CODE 字“内嵌扩展” [CUR93a] 。
内嵌扩展的缺点是:如果要把代码反编译回原始的代码就会非常困难。如果仅仅是使用子程序串线,我们依然可以得到指向 Forth 字的指针(子程序的地址)。通过字指针,就可以得到它们的名字。但是如果指令字扩展到内嵌编码中,所有的关于字来源的信息就全部丢失了。
除了速度和空间之外,内嵌扩展还有个优点:潜在的代码优化。例如: Forth 序列:
3 +
在 68000 STC 被编译成:
BSR LIT
.DW 3
BSR PLUS
但是,使用内嵌代码,就可以把它优化成一个机器指令。
Forth 编译器优化是一个广阔的领域,也是 Forth 语言研究中一个非常活跃的领域,这里不能完全讨论,可参见 [SCO89] 和 [CUR93b] 。优化 STC 的最终结果是能够产生“纯”机器代码的 Forth 编译器,就像 C 或者 Fortran 编译器一样。
标记串线编码( TTC )
DTC 和 STC 技术的目标是用一定的存储器消耗为代价来增加 Forth 程序的执行速度。现在让我们转向 ITC 的另一个方向:运行速度更慢、但代码尺寸更小。
Forth 串线的目的是指定一系列将要执行的 Forth 字(子程序)的地址。假设一个 16 位的 Forth 字最大只有 256 个 Forth 字,那么每个 Forth 字都可以用一个 8 位数来标识,我们就可以不使用 16 位的地址列表,而是用一系列的 8 位标识或者称为“标记( TOKEN )”来代替地址,这样冒号定义的代码尺寸就减少了一半。
在一个标记串线编码的 Forth 系统中,需要有一个记录所有 Forth 字的表格,如图 4 所示。标记值就是这个表项的索引,通过它来寻找一个指定标记对应的 Forth 字。这种方法为 Forth 解释器增加了一次间接访问,所以它比“地址串线”的 Forth 执行速度更慢。

图 4 DTC Forth 定义的存储
标记串线的基本优点是尺寸很小。 TTC 技术在手持计算机和其它对尺寸要求严格的应用中极为常见。同时,使用统一的 Forth 字“入口”表也简化了分开编译模块的链接。
TTC 的缺点是:速度慢。 TTC 的 Forth 系统速度是所有技术中最慢的, TTC 编译器也比其它技术的编译器更复杂一些。如果你的应用有多于 255 个 Forth 字定义,则还需要一些其它的编码方式来混合 8 位和更大的标记。
说到 TOKEN 串线,也许会想到的情况是 32 位的 Forth 系统通过 TOKEN 串线而使用 16 位的 Forth 代码,不过,实际上又有多少 32 位系统是存储器尺寸受限的呢?
段串线编码
由于曾经有许多的 Intel 8086 派生系统,我们也简单地提一下段串线技术。这种技术不再使用一个 64K 段内的“一般”字节地址,而是使用节地址(在 Intel 8086 中,一个节的大小是 16 个字节)。这样,解释器可以把这些地址装入段寄存器,而不是通常的地址寄存器。这就允许 16 位的 Forth 模型可以有效地访问 8086 的 1M 字节存储器。
段串线模型的基本缺点是 16 字节大小的存储器“粒度”,因为这种技术要求每个 Forth 字必须在 16 字节的边界上对齐,而一个 Forth 字又具有随机的长度,所以平均每个字要浪费 8 个字节。
寄存器分配
在讨论了各种串线技术之后, CPU 寄存器的分配和使用就是至关重要的设计考虑了。这可能也是最困难的。 CPU 寄存器的可用性又会反过来决定我们使用哪种串线技术,甚至决定我们使用哪种方式的存储器映射。
经典的 Forth 寄存器
经典的 Forth 虚拟机模型有 5 个“虚拟寄存器”。它们是 Forth 原语的抽象实体。 NEXT 、 ENTER 、 EXIT 就是用这些抽象寄存器定义的。
每个寄存器的宽度都是一个单元,也就是说,在 16 位 Forth 系统中,它们都是 16 位寄存器。(以后你会看到,也有一些特例)。它们不一定全部都是 CPU 寄存器,如果你的 CPU 没有足够的寄存器,其中一些可以保存在存储器中。本文将按照这些寄存器的重要性来描述,也就是说,在没有足够 CPU 物理寄存器的情况下,最后描述的寄存器应该最先考虑被放置到存储器中。
W 是工作寄存器 它可以被用来做很多事情。首先, W 寄存器应该是一个地址寄存器,应该能用 W 寄存器作为地址来读取和写入存储器;也需要用 W 寄存器做算术运算。在 DTC Forth 中,还要求能用 W 实现间接跳转。W 寄存器在每个 Forth 字中被解释器使用,如果 CPU 只有一个寄存器,那你也必须把这个唯一的寄存器用于W 寄存器 ,而把其它的寄存器放到存储器中,当然,这种实现会使整个系统慢得令人难以置信。
IP 是解释指针 它被每个 Forth 字使用(通过 NEXT 、 ENTER 、 EXIT )。 IP 必须是一个地址寄存器,你也需要增量 IP。子程序串线的 Forth 系统不需要这个寄存器。
PSP 是参数栈指针(或者叫数据栈指针) 有时也简称作 SP 。我使用 PSP 是由于“SP”通常都是 CPU 硬件寄存器的名字,而它们彼此是不能混淆的。大多数 CODE 字需要使用这个寄存器。 PSP 必须是一个堆栈指针,或者是能够增量和减量的地址寄存器。如果可以通过 PSP 进行索引寻址则会为系统带来有一些附加的好处。
RSP 是返回栈指针 有时也简称RP。在 ITC 和 DTC 的 Forth 系统中, RSP被冒号定义使用,在 STC 的 Forth 系统中,它被所有的字使用。 RSP 必须是一个堆栈指针,或者是能够增量和减量的地址寄存器。
如果可能,应该把 W 、 IP 、 PSP 、 RSP 都放到实际的 CPU 物理寄存器中,其它的虚拟寄存器可以保存在存储器中,当然,如果所有的寄存器都保持在 CPU 硬件寄存器中,将带来速度方面的好处。
X 寄存器是一个工作寄存器 不过这里并没有把它作为一个经典的 Forth 寄存器考虑,甚至在使用它作为二次间接的经典 ITC 实现中也没有被当做经典寄存器。在 ITC 中,必须能够使用 X 寄存器实现间接跳转。 X 寄存器也被几个 CODE 字作为算术运算操作的目的地址,在不能使用存储器作为操作数的处理器上是特别重要的。比如在 Z80 上,需要通过下面的方式来实现加法运算(用伪码表示):
POP W
POP X
X+W -> W
PUSH W
有时也定义另外一个寄存器 Y 。
UP 是用户指针 它保持当前任务的用户区基地址。 UP 通常的用法是加上一个偏移量后在高级 Forth 定义中使用它。如果 CPU 可以通过 UP 寄存器索引寻址, CODE 字就可以更简单和更快速地访问用户变量。如果你有多余的寄存器,可以用其中一个作为 UP 。单任务的 Forth 不需要 UP 。
如果需要 X ,则 X 应该优先于 UP 放入 CPU 物理寄存器。 UP 是 Forth 虚拟寄存器中最适合放入存储器的。
硬件堆栈的使用
许多 CPU 把堆栈指针作为硬件的一部分用于中断和子程序调用。如果把堆栈指针作为 Forth 的一个虚拟寄存器将会怎么样呢?它应该是 PSP 还是 RSP 呢?
这要根据具体情况来考虑。一般认为在 ITC 和 DTC 的 Forth 中, PSP 的使用比 RSP 更加频繁,如果你的 CPU 只有不多的寄存器, PUSH 和 POP 就会比显式地引用存储器速度更快,所以我们可以使用硬件堆栈作为参数栈。
另一方面,如果你的 CPU 有丰富的寻址方式,特别是允许进行索引寻址,就应该为 PSP 分配一个通用的地址寄存器,在这种情况下,应该使用硬件堆栈作为返回栈。
这里的结论对下面的情况不合适。比如在 TMS320C25 中,硬件堆栈的深度只有 8 个单元,这对于 Forth 系统来说基本上没有什么用途,所以它的硬件堆栈只能用于中断, PSP 和 RSP 都必须是通用的地址寄存器。注意 ANS Forth 规范中指定最小的参数栈是 32 个单元,返回栈是 24 个单元,而我选择的数据栈和返回栈都是 64 个单元。
有时你可能会遇到教条的说法,比如硬件堆栈“必须是参数栈”或者“必须是返回栈”。在这种情况下,你可以编写几个 Forth 原语比如: SWAP 、 OVER 、 @ 、 ! 、 + 、 0= 来看看哪种情况代码更小、速度更快。
顺便说一下,如果要做这种测试,字 DUP 和 DROP 价值不高。
偶尔你也会得到有趣的结论! Gary Bergstrom 指出在 MC6809 的 DTC 实现中,用 MC6809 的用户堆栈指针作为 IP 可以快几个周期,这里 NEXT 变成了 POP 。他使用索引指针作为 Forth 的堆栈指针。
把栈顶元素( TOS )放入寄存器
如果能把参数栈栈顶元素 TOS 放到寄存器中,则 Forth 的性能会得到明显改善。许多 Forth 字(比如 0= )将不再访问堆栈,其它的 Forth 字做同样的 PUSH 和 POP ,只不过在代码中的位置不同。只有不多的 Forth 字(比如 DROP 和 2DROP )变得比较复杂 -- 你必须同时更新 TOS 的内容。
把栈顶元素放到寄存器中之后,编写 CODE 字时需要遵循这样几个规则:
一个字从堆栈上移出一个项目时,必须弹出“新”的 TOS 到寄存器中;
一个字加入一个新的项目到堆栈上,必须把“旧”的 TOS 压入栈上 ( 当然,除非它被消耗掉 )
如果你的 CPU 至少有 6 个物理寄存器,我建议你保存 TOS 到其中一个寄存器中。我认为 TOS 比 UP 更重要,但它的重要性又次于 W 、 IP 、 PSP 、 和 RSP 寄存器。 TOS 寄存器执行了许多 X 寄存器的功能,如果这个寄存器可以实现存储器寻址就更加有用。 PDP-11 、 Z8 、 MC68000 处理器都是很好的例子。
Guy Kelly [KEL92] 研究了 19 个 IBM PC 上的 Forth 系统,其中有 9 个使用了 TOS 寄存器。
我认为, TOS 的想法没有广泛被接受的原因首先是下面一些错误的见解:
增加了指令;
栈顶元素必须通过存储器访问。
过分强调了PICK、ROLL 这些价值不高的字,说它们在 TOS 情况下必须进行重新编码。
如果把两个栈顶元素都放到寄存器中,结果会怎么样呢?当你这样做的时候,操作效率是相同的。一个 PUSH 仍然是一个 PUSH ,不论你在此前和以后进行了什么操作。另一方面,缓冲两个堆栈元素却增加了大量的代码:一个 PUSH 现在变成了一个 PUSH 后随一个 MOVE 。把两个元素缓冲到寄存器中,只有在 RTX2000 一类的 Forth 芯片上才有意义,其它的都是一些假想的、听起来似乎非常聪明、但在实际应用中没有什么意义的优化。
实际分配的一些例子
这里是一些不同的 CPU 寄存器分配实例,通过这个表,我们可以看出每个 Forth 系统作者的寄存器分配考虑。

[1] F83. [2] Pygmy Forth.
图 5 寄存器分配
“SP”指硬件堆栈指针。“Zpage”是指保存在 6502 存储器零页的值,零页几乎和寄存器一样有用,有时比寄存器更有用。比如,它们可以被用于存储器寻址。“Fixed” 指 Payne's 8051 Forth 有一个单一的、不可移动的用户区, UP 是硬编码的常数。
寄存器变窄
我们在上面的表格中注意到了什么奇怪的事情吗? 6502 Forth 是一个 16 位的模型,但是却使用了 8 位的栈指针。
在实际情况下,使 PSP 、 RSP 和 UP 的尺寸小于 Forth 的单元尺寸是可能的。这是因为堆栈和用户区相对于整个 CPU 可寻址存储器来说比较小。每个堆栈可以小到 64 个单元,而用户区很少超过 128 个单元。你只需要简单地相信:
这些数据区被限制在存储器的一个小的区域中,可以使用短的地址访问;
高地址位用其它的方式提供,比如,通过页面选择的方式来提供;
在 6502 CPU 中,硬件堆栈被 CPU 的设计者限定在 RAM 的一个页中(地址为 0x1xx )。8 位堆栈指针可以用作返回栈。参数栈保存在 RAM 的零页中,通过一个 8 位索引寄存器间接访问。
在 8051 中,你可以使用 8 位的寄存器 R0 和 R2 访问外部 RAM ,并显式地提供地址的高 8 位输出到 PORT 2 。这就允许对两个堆栈进行“页选择”。
UP 与 PSP 的 RSP 是有明显区别的:它只是简单地提供一个基地址,从来都不增量和减量。所以,它实际上只是提供这个虚拟寄存器的高位。低位必须借助某种索引技术来实现。例如,在 MC6809 中,你可以使用 DP 寄存器作为 UP 的高 8 位,然后使用直接页面寻址模式去访问这个页面中的 256 个位置。这就强制用户区域从 0x??00 开始,同时限制用户区域长度为 128 个单元, 这些都不是什么大问题。而在 Intel 8086 上,你还可以使用一个段寄存器作为用户区的基地址。
参考文献
[CUR93a] Curley, Charles, "Life in the FastForth Lane," awaiting publication in Forth Dimensions. Description of a 68000 subroutine-threaded Forth.
[CUR93b] Curley, Charles, "Optimizing in a BSR/JSR Threaded Forth," awaiting publication in Forth Dimensions. Single-pass code optimization for FastForth, in only five screens of code! Includes listing.
[KEL92] Kelly, Guy M., "Forth Systems Comparisons," Forth Dimensions XIII:6 (Mar/Apr 1992). Also published in the 1991 FORML Conference Proceedings . Both available from the Forth Interest Group, P.O. Box 2154, Oakland, CA 94621. Illustrates design tradeoffs of many 8086 Forths with code fragments and benchmarks -- highly recommended!
[KOG82] Kogge, Peter M., "An Architectural Trail to Threaded- Code Systems," IEEE Computer, vol. 15 no. 3 (Mar 1982). Remains the definitive description of various threading techniques.
[ROD91] Rodriguez, B.J., "B.Y.O. Assembler," Part 1, The Computer Journal #52 (Sep/Oct 1991). General principles of writing Forth assemblers.
[ROD92] Rodriguez, B.J., "B.Y.O. Assembler," Part 2, The Computer Journal #54 (Jan/Feb 1992). A 6809 assembler in Forth.
[SCO89] Scott, Andrew, "An Extensible Optimizer for Compiling Forth," 1989 FORML Conference Proceedings , Forth Interest Group, P.O. Box 2154, Oakland, CA 94621. Good description of a 68000 optimizer; no code provided.
Forth 实现
[CUR86] Curley, Charles, real-Forth for the 68000 , privately distributed (1986).
[JAM80] James, John S., fig-Forth for the PDP-11 , Forth Interest Group (1980).
[KUN81] Kuntze, Robert E., MVP-Forth for the Apple II , Mountain View Press (1981).
[LAX84] Laxen, H. and Perry, M., F83 for the IBM PC , version 2.1.0 (1984). Distributed by the authors, available from the Forth Interest Group or GEnie.
[LOE81] Loeliger, R. G., Threaded Interpretive Languages , BYTE Publications (1981), ISBN 0-07-038360-X. May be the only book ever written on the subject of creating a Forth-like kernel (the example used is the Z80). Worth it if you can find a copy.
[MPE92] MicroProcessor Engineering Ltd., MPE Z8/Super8 PowerForth Target , MPE Ltd., 133 Hill Lane, Shirley, Southampton, S01 5AF, U.K. (June 1992). A commercial product.
[PAY90] Payne, William H., Embedded Controller FORTH for the 8051 Family , Academic Press (1990), ISBN 0-12-547570-5. This is a complete "kit" for a 8051 Forth, including a metacompiler for the IBM PC. Hardcopy only; files can be downloaded from GEnie. Not for the novice!
[SER90] Sergeant, Frank, Pygmy Forth for the IBM PC , version 1.3 (1990). Distributed by the author, available from the Forth Interest Group. Version 1.4 is now available on GEnie, and worth the extra effort to obtain.
[TAL80] Talbot, R. J., fig-Forth for the 6809 , Forth Interest Group (1980).
|