Forth 系统实现 |
作者 Brad Rodriguez |
| 编译 赵宇 张文翠 |
第二部分 内核基准测试和个案研究
基准测试
我们已经回答了每个与 Forth 实现决策有关的问题,现在应该是“编码并查看结果”的时候了。不过,你肯定不想仅仅为了测试不同的方法就编写许多个完整的 Forth 内核。幸运的是,仅仅编写 Forth 内核的小子集就可以得到一些相当好的“感觉”。
Guy Kelly [KEL92] 研究了 19 个不同的 IBM PC 的下列一些代码样例:
NEXT …… 是链接“串线”中一个字到另一个字的“内层解释器”。用于每一个 CODE 定义的结尾,是决定 Forth 执行速度的一个最重要的因素。你已经看到了它的 ITC 和 DTC 伪码;在 STC 中,它就是 CALL/RET 指令。
ENTER …… 也称为 DOCOL 或者 DOCOLON ,高级“冒号”定义代码域动作。它对于速度也是至关重要的;用于每个冒号定义的开始,在 STC 中不需要。
EXIT …… 在 FIG-Forth 中称为 S; 。结束一个冒号定义执行的代码。它在每个冒号定义的结束处出现,决定高级子程序的返回效率。在 STC 中它就是一个 RET 机器指令。
NEXT 、 ENTER 和 EXIT 表现了串线机制的性能。它们都应该通过实际的编码来评估实现性能。它们也反映了实现时 IP 、 W 和 RSP 寄存器分配策略是否正确。
DOVAR …… “变量”,对于所有 Forth 变量 VARIABLE 的代码域动作。
DOCON …… “常量”,对于所有 Forth 常量 CONSTANT 的代码域动作。
DOCON 、 DOVAR 和 ENTER 一起显示了你可以得到一个正在执行的字的参数域地址的效率。这反映了你对 W 寄存器的选择,在 DTC Forth 中,也指出应该在代码域中放一个 JUMP 指令还是一个 CALL 指令。
LIT …… “文字量”。这个字从 Forth 的高级串线中取一个单元值。有几个字需要使用这样的内嵌参数,这很好地显示了它们的性能。它反映了你对 IP 寄存器的选择。
@ …… Forth 的存储器读取操作,显示了从高级 Forth 中访问存储器可以有多快。这个字常常从堆栈的 TOS 中受益。
! …… Forth 的存储器存操作,从另一方面反映了存储器访问的能力。它消耗堆栈的两个项目,所以能反映参数栈的访问效率。它也很好地说明了我们把 TOS 放在存储器还是放在寄存器中的决策。
+ …… 加法操作,是所有 Forth 算术和逻辑操作的典型代表。
以上是一个非常好的代码样例。我还增加了几个附属的测试:
DODEOS …… 是用 DOES> 构建字的代码域动作,尽管它没有反映 W 、 IP 和 RSP 的使用。我包含这个字是因为它是 Forth 内核中最费解的代码,如果你可以编码 DODOES 的逻辑,其它的任何东西就都不在话下了。 DODOES 的复杂性将在本文的后面描述。
SWAP …… 是一个简单的堆栈操作符,但能说明问题。
ROT …… 是一个更加复杂的堆栈操作符。它为你能简单地访问参数栈给出一个好主意。 ROT 好像需要一个外加的临时寄存器才能完成。如果你能够在不使用 X 寄存器的情况下实现 ROT ,则其它情况下也不会需要 X 寄存器。
0= …… 是不多的几个单目算术操作之一,是最有可能从“TOS 在寄存器中 ” 获益的字之一。
+! …… 是最多被说明的操作,组合了堆栈访问、算术、存储器取和存储器存。这是一个非常理想的用于标准测试的字,尽管比上面所列出的其它字使用频率低。
以上所列的都是最常用的 Forth 字,努力优化它们是值得的。我将给出一个 MC6809 的伪码例子。对于其它的处理器,我将解释特别选择的代码片段。
个案研究 1 : MC6809
在 8 位 CPU 世界中, MC6809 是 Forth 程序员的甜蜜之梦。它支持 2 个堆栈!还有另外 2 个地址寄存器和大量的只有 PDP-11 才有的正交寻址方式。正交的意思是指所有的地址寄存器有相同的选项和相同的工作方式,而两个 8 位累加器可以作为一个单一的 16 位累加器使用,并具有许多 16 位操作指令。
MC6809 的程序员模型是
A - 8 bit 累加器
B - 8 bit 累加器 大多数算术操作以累加作为目的寄存器。它们也可以连接在一起作为一个 16 位的累加器 D ( A 是高 8 位, B 是低 8 位)。
X - 16 位索引寄存器
Y - 16 位索寄存器
S - 16 位堆栈指针
U - 16 位堆栈指针 所有用于 X 和 Y 寄存器的寻址模式也可以用于 S 和 U 寄存器。
PC - 16 位程序计数器
CC - 8 位条件标志寄存器
DP - 8 位直接页访问寄存器
MC6800 系列的直接寻址模式可以使用一个 8 位寄存器访问零页存储器的任何位置。 MC6809 允许对任何页进行直接寻址。DP 寄存器提供高 8 位地址(页地址)。
有 2 个堆栈指针可供 Forth 使用,它们是等效的,但 CPU 设计者把 S 用于子程序调用和中断。为一致起见,我们把 S 作为返回栈, U 作为参数栈。
W 和 IP 都要求使用地址寄存器,它们逻辑上用于 X 和 Y 寄存器,我们可以任意指定:
X => W 而 Y => IP 。
现在来选择一个串线模型。我简单地舍弃 STC 和 TTC ,构造一个“传统”的 Forth 。性能上的限制因素是 NEXT 子程序。让我们先看看它的 ITC 和 DTC 实现:
ITC-NEXT:
LDX ,Y ++ (8) (IP) -> W, 增量 IP
JMP [,X] (6) (W) -> temp, 跳转到临时单元的地址
DTC-NEXT:
JMP [,Y++] (9) (IP)->temp, 增量 IP, 跳转到临时单元地址,临时单元在 MC6809 的内部。
NEXT 在 DTC 的 MC6809 中只有一条指令!这就意味着你可以用 2 个字节的内嵌编码,比 JMP NEXT 又快又好。作为比较,子程序串线是这样的:
RTS (5) ... 在 CODE 字的结尾
JSR nextword (8) ... 在串线中下一个 CODE 字的开始
STC 花费 13 个周期用于串线下的一个字,而 DTC 只需要 9 个周期。这是由于子程序串线需要将返回地址弹出和压栈,而 CODE 字却不需要。
决定了使用 DTC 之后,你还有两个选择:高级定义字在它的代码域中使用 JMP 还是 CALL ?决定的因素是我们如何能更快地得到后面的参数域地址。让我们注意一个冒号定义的 ENTER 编码:
如果使用 JSR (Call):
JSR ENTER (8)
...
ENTER:
PULS W (7) 得到 JSR 之后的地址到 W 中
PSHS IP (7) 保存旧的 IP 到返回栈
TFR W,IP (6) 参数域地址 -> IP
NEXT (9) JMP [,Y++] 的汇编语言智能
以上总计 37 个周期
如果使用 JMP:
JMP ENTER (4)
...
ENTER:
PSHS IP (7) 保旧的 IP 到返回栈上
LDX -2,IP (6) 重新得到代码域地址
LEAY 3,X (5) 加 3 存入 IP ( Y )寄存器中
NEXT (9)
以上总计 31 个周期
因为 MC6809 的寻址模式允许另外一级的间接,所以 6809 的 NEXT 不使用 W 寄存器。 ENTER 的 JMP 版本必须再次读取代码域的地址 -- NEXT 没有在任何寄存器中留下这个地址。 JSR 可以通过弹出返回栈直接得到参数域地址。所以, JMP 版本更快。
不论哪一种方式, EXIT 都是一样的:
EXIT:
PULS IP 从返回栈中弹出“保存的”IP
NEXT 继续 Forth 解释
有些寄存器尚未分配。你可以把用户指针放到存储器中,这样的 Forth 也运行得很好。不过 DP 寄存器就浪费了,而 DP 也没有什么其它的用处。让我们使用一个“技巧”来实现,我们把 UP 的高位搬到 DP 中(它的低字节是 0 )。
还有一个没有使用的寄存器 D 寄存器,许多算术操作需要这个寄存器。它应该自由地作为一个可随意使用的寄存器呢?还是应该作为栈顶元素呢? MC6809 使用存储器作为一个操作数,所以并不需要第二个工作寄存器。如果临时需要寄存器,把 D 压入和弹出也很容易。
所以我们只能对两种方式编写测试程序,看看哪个更快。
NEXT 、 ENTER 和 EXIT 不使用堆栈,在各种情况下的代码都是一样的。
DOVAR 、 DOCON 和 LIT 在两种情况下所用的时钟周期数相同。这就解释了我们以前谈到的把 TOS 放到寄存器中仅仅改变 PUSH 或者 POP 的位置:
SWAP 、 ROT 、 0= 、 @ 特别是 + 通过把 TOS 放到寄存器中而加快 :
但是, ! 和 +! 却由于 TOS 放到寄存器中而变慢 :
这些字变慢的原因是许多访问存储器的 Forth 字希望地址在栈顶,所以需要一个额外的 FTR 指令。这就是为什么 TOS 寄存器必须是一个地址寄存器。不幸的是, MC6809 的地址寄存器都用于更重要的 W 、 IP 、 PSP 和 RSP 了。不过,把 TOS 放到寄存器中对于 ! 和 !+ 的损失可以通过许多算术和堆栈操作运行速度的提高而得到弥补。
个案研究 2 : 8051
如果说 MC6809 是 Forth 系统实现者的美梦,那 Intel 8051 简直就是 Forth 实现者的恶梦了。它只有一个通用的地址寄存器,一种寻址模式,总是使用一个 8 位累加器。
所有的算术操作、许多的逻辑操作都必须使用累加器。一个唯一的 16 位操作是 INC DPTR 。硬件堆栈必须使用 128 字节的片内寄存器文件,这样的 CPU 简直就是一堆破铜烂铁!
有些 8051 Forth 实现了一个 16 位的 Forth ,但是它们太慢而不能满足我们的要求。让我们进行某些权衡,以产生一个更快的 8051 Forth 系统。
我们最初的想法是利用那个唯一的地址寄存器。所以我们用 8051 的程序计数器作为 IP -- 也就是说,我们构造一个子程序串线的 Forth 系统。如果编译器在所有可能的情况下都使用 2 字节的 ACALL 代替 3 字节的 LCALL ,多数的 STC 代码将和 ITC/STC 一样小。
子程序串线意味着返回栈指针就是硬件堆栈指针。片上寄存器文件共有 64 个单元空间,但是这些空间并不足以支持多任务堆栈。面对这种情况下你可以考虑以下几个策略:
限制这个 Forth 系统为单任务系统;
在所有的 Forth 定义入口处把返回地址保存到一个外部 RAM 软件堆栈中;
在任务切换的时候把全部返回栈的内容保存到外部 RAM 中。
第二种方法是最慢的!在每个任务切换的时候移动 128 个字节比在每个 Forth 字中移动两个字节要快得多。现在我选择 1 ,而将选择 3 留作以后扩充。
唯一一个真正的地址寄存器 DPTR 将要担负多种使命。它就是 W ,多用途的工作寄存器。
实际上,还有两个寄存器可以寻址外部存储器: R0 和 R1 。它们仅仅提供 8 位地址,高 8 位将显式地输出到口 2 上。但是对于堆栈,这是一个可以容忍的限制,因为我们可以把堆栈限制在 256 字节空间。所以我们使用 R0 作为 PSP 。
同样的 256 字节可以用于用户数据区,这使得 P2 (口 2 )成为用户指针的高字节,像 MC6809 一样,而低字节隐含是 0.
于是 8051 的程序员模型就变成了:
寄存器地址 8051 名字 Forth 使用
0 R0 PSP 的低字节
1 R1
2 R2
3 R3
4 R4
5 R5
6 R6
7 R7
8-7Fh 120 字节的返回栈
81h SP RSP 的低字节(高位字节 = 0 )
82-83h DPTR W 寄存器
A0h P2 UP 和 PSP 的高字节
E0h A
F0h B
注意我们仅仅使用了 BANK0 , 另外的 3 个寄存器 BANK 从 08H 到 1FH , 从 20H 到 2FH 的位寻址寄存器都没有被 Forth 使用。使用 BANK0 可以为返回栈得到最大的连续空间。如果需要,返回栈还可以缩小。
在子程序串线的 Forth 系统中,不需要 NEXT、ENTER 和 EXIT 。
如何处理栈顶元素呢?在 8051 中,有许多的寄存器,而存储器操作却非常昂贵。我们把 TOS 放到 R3:R2 中(按 INTEL 格式,R3 是高字节)。注意,我们不能使用 B:A 寄存器对 -- A 寄存器是一个漏斗,所有的寄存器引用都要通过它进行。
8051 采用了“哈佛”体系结构:程序和数据在分开的存储器中存放。(Z8 和 TMS320 是哈佛体系结构的另外两个例子)。但 8051 使用的是一种“野蛮”的退化形式:软件没有办法从物理上向程序存储器写,这就意味着 Forth 的开发者只能使用下述两个方式:
交叉编译全部程序,包括应用程序,放弃实现一个 8051 交互式 Forth 的努力;
使一部分或者全部的程序存储器在数据空间可见,最简单的办法就是使这两个空间完全覆盖。
相比 Z8 和 TMS320 就比较文明,它们允许向程序存储器写入。Forth 内核的具体实现将在以后讨论。
个案研究 3 : Z80
选择讨论 Z80 是因为它是非正交 CPU 的一个极端例子,它有 4 个不同种类的地址寄存器,有些操作使用寄存器 A 作为目的寄存器,有些则可以是任意的8位寄存器,有些是 HL 寄存器对,有些则可以是任意的16位寄存器,等等。有些操作(比如 EX DE, HL )却只允许一种寄存器组合。
在 Z80 这类的 CPU 中(或者同样在 8086 中), Forth 功能的指定必须仔细匹配 CPU 寄存器的能力。许多方案需要评估,而唯一的办法常常就是对不同的决策方案编写各种代码进行测试。为了避免本文变成为一堆“代码列表”,我选择了基于许多 Z80 编码经验的一种寄存器指定,它说明了这些选择可以合理地解释早期讨论的一般原理。
我希望得到一个传统的 Forth ,尽管我使用了直接串线技术。我需要全部的“经典”虚拟寄存器。
忽略其它的寄存器集, Z80 的6个地址寄存器具有下列能力:
BC,DE - LD A 间接 , INC, DEC 也交换 DE/HL
HL - LD r 间接 , ALU 间接 , INC, DEC, ADD, ADC, SBC, 交换 W/TOS, JP 间接
IX,IY - LD r 间接 , ALU 间接 , INC, DEC, ADD, ADC,SBC, 交换 W/TOS, JP 间接 ( 全都很慢 )
SP - PUSH/POP 16 位 , ADD/ADC/SUB to HL/IX/IY
BC, DE, 和 HL 也可以作为位寄存器对来处理。
8 位寄存器 A 必须留作临时寄存器,因为许多 ALU 操作和存储器引用操作都使用它作为目的。
HL 无疑是最通用的寄存器,可以逐个试着用它作为每个虚拟寄存器。然而,由于它的通用性 -- 它是唯一可以读取字格式和支持间接跳转的寄存器 -- HL 应该作为 Forth 的通用工作寄存器 W 。
由于 IX 、 IY 都有索引寻址模式并可用 ALU 操作,所以可以考虑用它们作为堆栈指针 。但是它们有两个主要的问题:通用的堆栈指针 SP 寄存器没有用,而 IX/IY 却特别慢!
在 Forth 的两个栈上都有许多 16 位的 PUSH / POP 类操作,对于 SP 来说,这些操作只需要一条指令,而 IX 或者 IY 操作却需要 4 条指令。所以两个堆栈之一应该用 SP 实现,这应该是参数栈,因为它的使用频率比返回栈要高。
如何考虑 Forth 的 IP 寄存器呢?在大多数情况下, IP 都是从存储器读取并且自动增量的,使用 IX/IY 作为 IP 不会比使用 BC/DE 有任何编程上的好处,考虑 IP 的速度,使用 BC/DE 对却更快。让我们把 IP 放到 DE 中:它可以与 HL 的内容交换,而后者是通用的。
需要第二个 Z80 寄存器对(不是 W )进行 16 位的算术运算。现在只有 BC 了,它可以用于寻址或者与 A 进行 ALU 操作。但是,我们是用 BC 作为第二个工作寄存器“X”、还是作为栈顶元素呢?只有编码才能得到结论。现在,让我们乐观地假定 BC = TOS 。
只剩下 RSP 和 UP 了,还有 IX 和 IY 寄存器没有分配。 IX 和 IY 是等效的,我们设 IX = RSP , IY = UP 。
于是, Z80 Forth 系统的寄存器分配如下
BC = TOS IX = RSP
DE = IP IY = UP
HL = W SP = PSP
现在让我们看看 DTC 的 Forth 系统 NEXT 编码:
DTC-NEXT:
LD A,(DE) (7) (IP)->W, 增量 IP
LD L,A (4)
INC DE (6)
LD A,(DE) (7)
LD H,A (4)
INC DE (6)
JP (HL) (4) 跳转到 W 中的地址
还可以有其它的版本(具有同样的时钟周期)
DTC-NEXT:
EX DE,HL (4) (IP)->W, 增量 IP
NEXT-HL:
LD E,(HL) (7)
INC HL (6)
LD D,(HL) (7)
INC HL (6)
EX DE,HL (4)
JP (HL) (4) 转到 W 中的地址
注意单元是以低位字节优先的方式存放在存储器中的。同样,尽管看起来把 IP 保存在 HL 寄存器中有许多好处,但实际上却没有。这是由于 Z80 不能进行 JP (DE) 。 NEXT-HL 进入将更短一些。
仅仅用于比较,让我们看一下 ITC NEXT 。以前给出的伪代码需要另一个临时寄存器“X”,它的内容用于间接跳转。令 DE = X, BC = IP, TOS 保存在存储器中。
ITC-NEXT:
LD A,(BC) (7) (IP)->W, 增量 IP
LD L,A (4)
INC BC (6)
LD A,(BC) (7)
LD H,A (4)
INC BC (6)
LD E,(HL) (7) (W)->X
INC HL (6)
LD D,(HL) (7)
EX DE,HL (4) 跳转到 X 中的地址
JP (HL) (4)
这就把“W”加 1 并放到在 DE 寄存器中了。只要这是一致的,就不会有任何问题 -- 代码知道在需要 W 的内容时如何去找到它,以及如何调整它。
ITC 的 NEXT 是 11 个同期, DTC 是 7 个同期。 ITC 没有将 TOS 保存在寄存器中的能力,所以我选择 DTC 。
如果使用内嵌编码, DTC NEXT 在每个 CODE 字中需要 7 个字节。一个直接跳转到 NEXT 的子程序只需要 3 个字节,但需要附加 10 个时钟周期,这是一个特别的例子,我们选择的是内嵌方式的 NEXT 。但有时 NEXT 特别大,或者存储器很小,更谨慎的决策可能是使用 JMP 到 NEXT 。
现在让我们来看 ENTER 的代码。使用一个 CALL ,可以弹出硬件堆栈以得到参数域地址:
CALL ENTER (17)
...
ENTER:
DEC IX (10) 把老的 IP 放到返回栈上
LD (IX+0),D (19)
DEC IX (10)
LD (IX+0),E (19)
POP DE (10) 参数域地址 -> IP
NEXT (38) 7 个机器指令的汇编语言宏
实际上这比 POP HL 快,然而使用最后的 6 个指令(不用 EXDE , HL ):
CALL ENTER (17)
...
ENTER:
DEC IX (10) 把老的 IP 放到返回栈上
LD (IX+0),D (19)
DEC IX (10)
LD (IX+0),E (19)
POP HL (10) 参数域地址 -> HL
NEXT-HL (34) 看上面的 DTC 的 NEXT 代码
总计 119 个周期
当使用 JP 时, W 寄存器( HL )依然指向代码域。代码域是其后的 3 个字节:
JP ENTER (10)
...
ENTER:
DEC IX (10) 把老的 IP 放到返回栈上 LD (IX+0),D (19)
DEC IX (10)
LD (IX+0),E (19)
INC HL ( 6) 参数域地址 -> IP
INC HL ( 6)
INC HL ( 6)
NEXT-HL (34)
总计 120 个周期
由于改变了 NEXT 的入口, IP 的新值就不必放入 DE 寄存器对了。
CALL 版本快了 1 个周期。在嵌入式系统应用 Z80 时,我们还可以使用单字节的 RST 指令来得到速度和空间的双重收益,但是在基于 Z80 的个人计算机上,这个策略并不可用(操作系统使用了这个特性,即操作系统的系统调用是通过这个接口进入的)。
个案研究 4 : INTEL 8086
Intel 的 8086 是另一个有教育意义的 CPU 。我们不再详细讨论设计过程,只是看一个新的用于 PC 的共享软件: Pygmy Forth [SER90].
Pygmy 是一个直接串线的 Forth 系统,栈顶元素保存在寄存器中。 8086 寄存器是这样安排的:
AX = W DI = scratch
BX = TOS SI = IP
CX = scratch BP = RSP
DX = scratch SP = PSP
许多 8086 Forth 系统的实现使用 SI 寄存器作为 IP ,所以 NEXT 可以通过 LODSW 指令实现。在 Pygmy 的 DTC 实现中, NEXT 是这样的:
NEXT:
LODSW
JMP AX
这已经小得足以嵌入到每个 CODE 字中了 。
高级“定义” Forth 字使用一个 JMP (相对)指令转向它们的机器码。 ENTER 子程序(在 Pygmy 中称为 'docol' )因此需要从 W 中得到参数域地址。
ENTER:
XCHG SP,BP
PUSH SI
XCHG SP,BP
ADD AX,3 参数域地址 -> IP
MOV SI,AX
NEXT
注意交换两个堆栈指针的 XCHG 用法,这允许对两个堆栈都使用 PUSH 和 POP 指令,这比使用基于 BP 的直接寻址指令要快。
EXIT:
XCHG SP,BP
POP SI
XCHG SP,BP
NEXT
段模型
Pygmy Forth 是一个单段的 Forth 系统,所有的代码和数据都在一个 64K 字节的段中, 这相当于 Turbo C 的紧缩模式。到目前为止,我们讨论的 Forth 标准都假设所有的东西全部包含在单一的存储器地址空间,使用同样的读写操作符。然而, IMP PC Forth 开始使用多个段来处理 5 种不同的数据,它们是:
CODE …… 机器代码
LIST …… 高级 Forth 串线 ( 所以这个段也称为 THREADS)
HEAD …… Forth 字的首部
STACK …… 参数和返回栈
DATA …… 变量和用户定义数据
这就允许 PC 机上的 Forth 突破 64K 字节的段限制,而又不需要在一个 16 位的 CPU 上实现一个 32 位的 Forth 系统。但是,实现一个多段的模型、分支到 Forth 核心等等内容已经远远超出了本文的讨论范围。
参考文献
[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!
[MOT83] Motorola Inc., 8-Bit Microprocessor and Peripheral Data , Motorola data book (1983).
[SIG92] Signetics Inc., 80C51-Based 8-Bit Microcontrollers , Signetics data book (1992).
Forth 实现
[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.
[SEY89] Seywerd, H., Elehew, W. R., and Caven, P., LOVE-83Forth for the IBM PC , version 1.20 (1989). A shareware Forth using a five-segment model. Contact Seywerd Associates, 265 Scarboro Cres., Scarborough, Ontario M1M 2J7 Canada.
|