# 4.3.3 SEQ 的时序

在介绍图 4-18~图 4-21 的表时,我们说过要把它们看成是用程序符号写的,那些赋值是从上到下顺序执行的。然而,图 4-23 中硬件结构的操作运行根本完全不同,一个时钟变化会引发一个经过组合逻辑的流,来执行整个指令。让我们来看看这些硬件怎样实现表中列出的这一行为。

SEQ 的实现包括组合逻辑和两种存储器设备:时钟寄存器(程序计数器和条件码寄存器),随机访问存储器(寄存器文件、指令内存和数据内存)。组合逻辑不需要任何时序或控制——只要输入变化了,值就通过逻辑门网络传播。正如提到过的那样,我们也将读随机访问存储器看成和组合逻辑一样的操作,根据地址输入产生输出字。对于较小的存储器来说(例如寄存器文件),这是一个合理的假设,而对于较大的电路来说,可以用特殊的时钟电路来模拟这个效果。由于指令内存只用来读指令,因此我们可以将这个单元看成是组合逻辑。

现在还剩四个硬件单元需要对它们的时序进行明确的控制——程序计数器、条件码寄存器、数据内存和寄存器文件。这些单元通过一个时钟信号来控制,它触发将新值装载到寄存器以及将值写到随机访问存储器。每个时钟周期,程序计数器都会装载新的指令地址。只有在执行整数运算指令时,才会装载条件码寄存器。只有在执行 rmmovqpushqcall 指令时,才会写数据内存。寄存器文件的两个写端口允许每个时钟周期更新两个程序寄存器,不过我们可以用特殊的寄存器 ID 0xF 作为端口地址,来表明在此端口不应该执行写操作。

要控制处理器中活动的时序,只需要寄存器和内存的时钟控制。硬件获得了如图 4-18~图 4-21 的表中所示的那些赋值顺序执行一样的效果,即使所有的状态更新实际上同时发生,且只在时钟上升开始下一个周期时。之所以能保持这样的等价性,是由于 Y86-64 指令集的本质,因为我们遵循以下原则组织计算:

原则:从不回读

处理器从来不需要为了完成一条指令的执行而去读由该指令更新了的状态。

这条原则对实现的成功来说至关重要。为了说明问题,假设我们对 pushq 指令的实现是先将 %rsp 减 8,再将更新后的 %rsp 值作为写操作的地址。这种方法同前面所说的那个原则相违背。为了执行内存操作,它需要先从寄存器文件中读更新过的栈指针。然而,我们的实现(图 4-20)产生出减后的栈指针值,作为信号 valE,然后再用这个信号既作为寄存器写的数据,也作为内存写的地址。因此,在时钟上升开始下一个周期时,处理器就可以同时执行寄存器写和内存写了。

再举个例子来说明这条原则,我们可以看到有些指令(整数运算)会设置条件码,有些指令(跳转指令)会读取条件码,但没有指令必须既设置又读取条件码。虽然要到时钟上升开始下一个周期时,才会设置条件码,但是在任何指令试图读之前,它们都会更新。

以下是汇编代码,左边列出的是指令地址,图 4-25 给出了 SEQ 硬件如何处理其中第 3 和第 4 行指令:

1  0x000:       irmovq $0x100, %rbx      # %rbx <-- 0x100
2  0x00a:       irmovq $0x200, %rdx      # %rdx <-- 0x200
3  0x014:       addq %rdx, %rbx          # %rbx <-- 0x300 CC <-- 000
4  0x016:       je dest                   # Not taken
5  0x01f:       rmmovq %rbx, 0(%rdx)     # M[0x200] <-- 0x300
6  0x029: dest: halt

标号为 1~4 的各个图给出了 4 个状态单元,还有组合逻辑,以及状态单元之间的连接。组合逻辑被条件码寄存器环绕着,因为有的组合逻辑(例如 ALU)产生输入到条件码寄存器,而其他部分(例如分支计算和 PC 选择逻辑)又将条件码寄存器作为输入。图中寄存器文件和数据内存有独立的读连接和写连接,因为读操作沿着这些单元传播,就好像它们是组合逻辑,而写操作是由时钟控制的。

跟踪 SEQ 的两个执行周期

图 4-25 跟踪 SEQ 的两个执行周期。每个周期开始时,状态单元(程序计数器、条件码寄存器、寄存器文件以及数据内存)是根据前一条指令设置的。信号传播通过组合逻辑,创建出新的状态单元的值。在下一个周期开始时,这些值会被加载到状态单元中

图 4-25 中的不同颜色的代码表明电路信号是如何与正在被执行的不同指令相联系的。我们假设处理是从设置条件码开始的,按照 ZF、SF 和 OF 的顺序,设为 100。在时钟周期 3 开始的时候(点 1),状态单元保持的是第二条 irmovq 指令(表中第 2 行)更新过的状态,该指令用浅灰色表示。组合逻辑用白色表示,表明它还没有来得及对变化了的状态做出反应。时钟周期开始时,地址 0x014 载入程序计数器中。这样就会取出和处理 addq 指令(表中第 3 行)。值沿着组合逻辑流动,包括读随机访问存储器。在这个周期末尾(点 2),组合逻辑为条件码产生了新的值(000),程序寄存器 %rbx 的更新值,以及程序计数器的新值(0x016)。在此时,组合逻辑已经根据 addq 指令被更新了,但是状态还是保持着第二条 irmovq 指令(用浅灰色表示)设置的值。

当时钟上升开始周期 4 时(点 3),会更新程序计数器、寄存器文件和条件码寄存器,因此我们用蓝色来表示,但是组合逻辑还没有对这些变化做出反应,所以用白色表示。在这个周期内,会取出并执行 je 指令(表中第 4 行),在图中用深灰色表示。因为条件码 ZF 为 0,所以不会选择分支。在这个周期末尾(点 4),程序计数器已经产生了新值 0x01f。组合逻辑已经根据 je 指令(用深灰色表示)被更新过了,但是直到下个周期开始之前,状态还是保持着 addq 指令(用蓝色表示)设置的值。

如此例所示,用时钟来控制状态单元的更新,以及值通过组合逻辑来传播,足够控制我们 SEQ 实现中每条指令执行的计算了。每次时钟由低变高时,处理器开始执行一条新指令。