第 4 章
C H A P T E R 4 ·
处理器体系结构 #
现代微处理器可以称得上是人类创造出的最复杂的系统之一。一块手指甲大小的硅片上,可以容纳一个完整的高性能处理器、大的高速缓存,以及用来连接到外部设备的逻辑电路。 从性能上来说 , 今天在一块芯片上实现的处理器已经使 20 年前价值 1000 万美元、房间那么大的超级计算机相形见细了。即使是在像手机、导航系统和可编程恒温器这样的日常设备中的嵌入式处理器,也比早期计算机开发者所能想到的强大得多。
到目前为止,我们看到的计算机系统只限于机器语言程序级。我们知道处理器必须执行一系列指令,每条指令执行某个简单操作,例如两个数相加。指令被编码为由一个或多 个字节序列组成的二进制格式。一个处理器支持的指令和指令的字节级编码称为它的指令 集体系 结构 (I nst ru ction-Set Architecture, ISA)。不同的处理器“ 家族”, 例如 In tel IA32 和 x86-64 、IBM/ Freescale Power 和 ARM 处理器家族, 都有不 同的 ISA。一个程序编译成在一种机器上运行,就不能在另一种机器上运行。另外,同一个家族里也有很多不同型 号的处 理器。虽然每个厂商制造的处理器性能和复杂性不断提高, 但是不同的型号在 ISA 级别上都保持着兼容。一些常见的处理器家族(例如x86-64) 中的处理器分别由多 个厂商提供。因此, ISA 在编译器编写者和处理器设计人员之间提供了一个概念抽象层,编 译器编写者只需要知道允许哪些指令,以及它们是如何编码的;而处理器设计者必须建造出执行 这些指令的处理器。
本章将简要介绍处理骈硬件的设计。我们将研究一个硬件系统执行某种 ISA 指令的方
式。这会使你能更好地理解计算机是如何工作的,以及计算机制造商们面临的技术挑战。 一个很重要的 概念是 , 现代处理器的实际工作方式可能跟 ISA 隐含的计算模型大相径庭。
ISA 模型看上去应该是顺序指 令执行, 也就是先取出一条指令,等 到它执行完毕 ,再 开始下一条。然而,与一个时刻只执行一条指令相比,通过同时处理多条指令的不同部分,处 理器可以获得更高的性能。为了保证处理器能得到同顺序执行相同的结果,人们采用了一 些特殊的机制。在计算机科学中,用巧妙的方法在提高性能的同时又保待一个更简单、更 抽象模型的功能 , 这种思想是众所周知的。在 Web 浏览器或平衡二叉树和哈希表这样的信息检索数据结构中使用缓存,就是这样的例子。
你很可能永远都不会自己设计处理器。这是专家们的任务,他们 工作在全球不到 100
家的公司里。那么为什么你还应该了解处理器设计呢?
- 从智力方面来说,处理器设计是非常有趣而且很重要的。学习事物是怎样工作的 有其内在价值。了解作为计算机科学家和工程师日常生活一部分的一个系统的内 部工作原理(特别是对很多人来说这还是个谜),是件格外有趣的事情。处理器设计包括许多好的工程实践原理。它需要完成复杂的任务,而结构又要尽可能简单 和规则。
- 理解处理器如何工作 能帮助 理解整个计算机 系统如何 工作。在第 6 章, 我们将讲述存储器系统,以及用来创建很大的内存映像同时又有快速访问时间的技术。看看处 理器端的处理器 内存接口,会使那些讲述更加完整。
- 虽然很少有人设计处理器,但是许多人设计包含处理器的硬件系统。将处理器嵌入到现实世界的系统中,如汽车和家用电器,已经变得非常普通了。嵌入式系统的设计者必须了解处理器是如何工作的,因为这些系统通常在比桌面和基千服务器的系统更低抽象级别上进行设计和编程。
你的工作可能就是处理器设计。虽然生产处理器的公司很少,但是研究处理器的设计人员队伍已经非常巨大了,而且还在壮大。一个主要的处理器设计的各个方面大约涉及 1000 多人。
本章首先定义一个简单的指令集, 作为我们处理器实现的运行示例。因为受 x86-6 4 指令集的启发, 它被俗称为 " x86" , 所以我们称我们的指令集为 " Y86-64" 指令集。与x86-64 相比, Y86-64 指令集的数据类型、指令和寻址方式都要少一些。它的字节级编码也比较简单, 机器代码 没有相应的 x86- 64 代码紧凑 , 不过设计它的 CPU 译码逻辑也要 简单一些。虽然 Y86- 64 指令集很简单, 它仍然足够完整, 能让我们写一些处理整数的 程序。设计一个实现 Y86-64 的处理器要求我们解决许多处 理器设计者同样会面对的问 题。
接下来会提供一些数字硬件设计的背景。我们会描述处理器中使用的基本构件块,以及它们如何连接起来和操作。这些介绍是建立在第 2 章对 布尔代数和位级操作的讨论的基础上的。我们还将介 绍一种描述硬件系统控制部分的简单语言, H CL ( Hardwa re Control
Language, 硬件控制 语言)。然后,用 它来描述我们的处理器设计。即使你已经 有了一些逻辑设计的背景知识,也 应该读读这个部分以 了解我们的 特殊符号表示方法。
作为设计处理器的第一步,我们给出一个基于顺序操作、功能正确但是有点不实用的Y86-64 处理器。这个处理器每个时钟周期 执行一 条完整的 Y86-64 指令。所以它的时钟必须足够慢,以允许在一个周期内完成所有的动作。这样一个处理器是可以实现的,但是它的性能远远低于同样的硬件应该能达到的性能。
以这个顺序设计为基础, 我们进行一系列的改造,创 建 一个流水 线化的 处理 器 ( pipe lined pro cessor ) 。这个处理器将每条指令 的执行分解成五步, 每个步骤由一个独立的硬件部分或阶段( stage )来处理。指令步经流水线的各 个阶段, 且每个时钟周期有一条新指令进入流水线。所以,处理器可以同时执行五条指令的不同阶段。为了使这个处理器保留 Y86-64 IS A 的顺序行为, 就要求处理很多冒险或 冲突( hazard ) 情况, 冒险就是一条指令的位置或操作数依赖于其他仍在流水线中的指令。
我们设计了一些工具来研究 和测试处理器设计。其中包括 Y86-64 的汇编器、在你的机器上运行 Y86-64 程序的模拟器, 还有针对两个顺序处理器设计和一个流水线化处理器设计的模 拟器。这些设计的控制逻辑用 HCL 符号表示的 文件描述。通过编辑这些文件和重新编译模拟器,你可以改变和扩展模拟器行为。我们还提供许多练习,包括实现新的指令和修改机器处理指令的方式。还提供测试代码以帮助你评价修改的正确性。这些练习将极大地帮助你理解所有这些内容, 也能使你更理解处理器设计者面临的许多不同的设计选择 。
网络旁注 ARC H : VLOG 给出了用 Verilog 硬件描述语言描述的流水线化的 Y86-64 处
理器。其中包括为基本的硬件构建块和整个的处理器结构创建模块。我们自动地将控制逻辑的 H CL 描述翻译成 Ver ilog 。 首先用我们的模拟器调试 H CL 描述, 能消除很多在硬件设计中会出现的棘手的问题。给定一 个 Verilog 描述, 有商业和开源工具来支待模拟和逻辑合成(l ogic synthesis), 产生实际的微处理器电路设计。因此,虽然我们在此花费大部分精力创建系统的图形和文字描述,写软件的时候也会花费同样的精力,但是这些设计能够自动地合 成, 这表明我们确实 在创建一个能 够用硬件实 现的系统。
4. 1 Y86-64 指令集体系结构
定义一个指令集体系结构(例如 Y86- 64 ) 包括定义各种状态单元、指令集和它们的编码、一组编程规范和异常事件处理。
1. 1 程序员可见的状态
如图 4-1 所示, Y8 6- 64 程序中的每条指令都会读取或修改处理器状态的某些部分。这称为程序员可见状态,这里的"程序员”既可以是用汇编代码写程序的人,也可以是产生 机器级代 码的编译器。在处理器实现中, 只 要 RF: 程序寄存器
我们保证机器级程序能够访问程序员可见状
态, 就不需要完全按照 ISA 暗示的方式来表示和组织 这个处理器状态。Y8 6-64 的状态类似千 x86 -64 。有 15 个程序寄存器:%r a x 、%r c x 、%
r dx、%r b x 、%r s p、r% b p 、r% s i 、r% d i 和 %r 8 到
%r1 4。(我们省略了 x8 6-64 的寄存器%r l 5 以 简化指令的 编码。)每个程序寄存器存储一个 64
CC: 条件码
Stat: 程序状态
I I
DMEM: 内存
位的字。寄存器%r s p 被入栈、出栈、调用和 PC
返回指令作为栈指针。除此之外,寄存器没有
固定的含义或固定值。有 3 个一位的条件码: 图 4-1 Y86-64 程序员可见状态。同 x86-64 一
样, Y86-64 的程序可以 访问 和修改 程
ZF 、 SF 和 OF , 它们保存着最近的算术或逻辑
序寄存器、条件码、程序计数器 ( P C )
指令所造 成影响的有关信息。程序计数器( P C ) 和内存。状态码指明程序是否运行正存放当前正在执行指令的地址。 常,或者发生了某个特殊事件
内存从概念上来说就是一个很大的字节数组, 保存着程序和数据。Y86-64 程序用虚拟地址 来引用内存位置。硬件和操作系统软件联合起来将虚拟地址翻译成实际或物理地址, 指明数据实际存在内存中哪个地方。第 9 章将更 详细地研究虚拟内存。现在, 我们只认为虚拟内 存系统向 Y86-64 程序提供了一个单一的字节数组映像。
程序状态的最后一个部分是状态码 St a t , 它表明程序执行的总体状态。它会指示是正常运行 , 还是出现了某种异常, 例如当一条指令试图去读非法的内存地址时。在 4. 1. 4 节中会讲 述可能的状态码以及异常处理。
4. 1: 2 Y86-64 指 令 #
图 4-2 给出了 Y86-64 IS A 中各个指令的简单描述。这个指令集就是我们处理器实现的目标。Y86- 64 指令集基本上是 x86- 64 指 令集的一个子集。它只包括 8 字节整数操作, 寻址方式 较少,操 作 也 较少。因为我们只有 8 字节数据,所 以 称之为 ”字 ( w o r d ) " 不 会 有任何歧 义。在这个图中,左 边 是 指 令 的 汇 编 码 表示, 右 边是字节编码。图 4-3 给出了其中一些指令更详细的内容。汇编代码格式类似于 x86-64 的 AT T 格式。
下面是 Y86- 64 指令的一些细节。
- x86-64 的 movq 指 令 分 成 了 4 个不同的指令: i rmovq、r rmovq 、mrmovq 和 rmmovq , 分别 显式 地指 明 源和目的的格式。源可以是立即数(立、寄存器 (r ) 或内存 Cm) 。 指 令名字的第一个字母就表明了源的类型。目的可以是寄存器(r ) 或内存Cm) 。指 令 名字的第二个字母指明了目的的类型。在决定如何实现数据传送时,显式地指明数据传送的
这 4 种类型是很有帮助的。
两个内存传送指令中的内存引用方式是简单的基址和偏移量形式。在地址计算中, 我们不支持第二变址 寄存 器 ( s eco nd index regis t e r ) 和 任 何 寄 存 器 值 的 伸缩( s ca ling ) 。
同 x8 6-64 一样, 我们不允许从一个内存地址直接传送到另一个内存地址。另外,也不允许将立即数传送到内存。
有 4 个整数操作指令, 如图 4- 2 中 的 OPq 。 它 们 是 a d d q 、 s u b q 、 a n d q 和 xo r q。它们只 对寄存器数据进行操作, 而 x8 6-64 还允许对内存数据进行这些操作。这些指令会设置 3 个条件码 ZF 、 S F 和 OF( 零 、符 号 和 溢出)。
7 个跳转指令(图4-2 中的 环x ) 是 j mp 、 j l e 、 j l 、 j e 、 j n e 、 j g e 和 j g。根据分支指令 的 类 型 和条件代码的设置来选择分支。分支条件和 x86- 64 的一样(见图3-15)。
有 6 个条件传送指令(图 4- 2 中的 c mo v XX) : c mo v l e 、 c mo v l 、 c mo v e 、 c mov ne 、c mo v g e 和 c mo v g 。 这些指令的格式与寄存器-寄存器传送指令r r mo v q 一 样 ,但是只有当条件码满足所需要的约束时,才会更新目的寄存器的值。
c a l l 指令将返回地址入栈,然 后 跳到 目 的 地址。r e t 指令从这样的调用中返回。
p u s hq 和 p o p q 指令实现了入栈和出栈,就 像 在 x8 6- 64 中 一 样 。
h a l t 指 令 停 止 指 令 的 执 行 。 x8 6-64 中有一个与之相当的指令 h lt 。x8 6- 64 的应用程序不允许使用这条指令,因 为它会导致整个系统暂停运行。对 千 Y8 6-64 来说,
执 行 h a lt 指 令 会 导 致处理器停止,并 将 状 态 码 设 置 为 HL T( 参见 4. 1. 4 节)。字节 。 1 2 3 4
ha l 七 荨
nop 荨
rrrnovq rA, rB 2 Io rAI rBI ir mov q V, rB 3 jo F j rBj rmmovq rA, D(rB) 4 jo rAj rBj
mr mo v q D(rB), rA 5 lo rAlrBI OPq rA, rB 6 I fn J rAI rBI
jXX Dest 尸
cmovxx rA, rB j2 I fn I rAI rBI c a ll Dest 巨
ret 荨
pushq rA I A 伈 l rAIF I
p o pq rA IB l o lrAIF I
Dest
Dest
图 4- 2 Y8 6-64 指令集。指令编码长度从 1 个字节到 10 个字节不等。一条指令含有一个单字节的指令指示符, 可能含有一个单 字节的寄存器指示符, 还可能含有一个8 字节的常数字。字段 fn 指明是某个整数操作 ( OPq ) 、数据传送条件( c movXX) 或是分支条件 ( j XX) 。 所有的数值都
用十六进制表示
4. 1. 3 指令编码
图 4-2 还给出了指令的字节级编码。每条指令需要 1 - 10 个字节不等, 这取决于需要哪 些 字 段 。 每条指令的第一个字节表明指令的类型。这个字节分为两个部分, 每部分 4
位: 高 4 位是代码( cod e ) 部分, 低 4 位是功能( fu nc t io n ) 部分。 如图 4- 2 所示, 代码值为O~ Ox B。功能 值只有在一组相关指令共用一个代码时才有用 。图 4-3 给出了整数操作、分支和条件传送指令的 具体编码。可以 观察到,r r mo v q 与条件传送有同样的指令代码。可
以把它看作是一个 “无条件传送“, 就好像 j mp 指令是无条件跳转一样,它 们的功能代码都是 0。
整数操作指令 分支指令 传送指令
addq I 6 1 O I jmp I 7 I O I jne I 7 I 4 I rrrnovql 2 。cmovne j 2 j 4 j
s ubq 曰巨] j l e 巨巨] j g e 巨卫] cmovleI 2 1 cmovge I 2 I 5 I
andq I 612 I jl 1 7 1 2 1 jg 1 7 1 6 1 cmovl I 2 2 cmovg I 2 I 6 I
xorq I 613 I j e I 7 I 3 I cmove I 2 3
图 4- 3 Y86-64 指令集的功能 码。这些代码指明是某个整数操作、分支 条件还是数据传送条件。这些指令是图 4-2 中所 示的 OPq 、 j XX 和 cmovXX
如图 4- 4 所示, 1 5 个程序寄存 器中每个都有一 个相对应的范围在 0 到 Ox E 之间的寄存器标识符 ( reg is te r ID ) 。Y8 6-6 4 中的寄存器编号跟 x86- 64 中的相同。程序寄存器存在
CPU 中的一个寄存器文件 中, 这个寄存器文件就是一个小的、以寄存器 ID 作为地址的随机访问存储器。在指令编码中以及在我们的硬件设计中,当需要指明不应访问任何寄存器 时,就用 ID 值 Ox F 来表示。
图 4-4 Y86-64 程序寄存器标识符。 1 5 个程序寄存器中每个都有一个相对应的标识符 ( ID )’ 范 围 为
O~ OxE。如果指令中某个寄存器字段的 ID 值为 OxF, 就表明此处没有寄存器操作数
有的指令只 有一个字节长 ,而有 的需要操作数的指令编码就更长一些。首先, 可能有附加的寄存 器指 示符 字 节 ( r eg is t er specifier byte), 指定一个或两个寄存器。在图 4- 2 中, 这些寄存器字段 称为 rA 和 rB。从指令的汇编代码表 示中可以看到, 根据指令类型, 指令可以指定用于数据源和目的的寄存器,或是用千地址计算的基址寄存器。没有寄存器操作数的指令 , 例如分支指令和 c a l l 指令, 就没有寄存器指示符字节。那些只需要一个寄存器操作数的 指令Cir mo v q 、 p u s h q 和 p o p q ) 将另一个寄存器指示符设为 Ox F 。 这种约定在我们的处理器实现中非常有用。
有些指令需 要一个附加的 4 字节常数 字 ( co n s ta nt w or d ) 。这个字能作为 ir mo v q 的立即数数 据,r mmov q 和 mr mo v q 的 地址指示符的偏移量,以 及分支指令和调用指令的目的地址。注意 ,分 支指令和调用指令的目的是一个绝对地址, 而不像 IA32 中那样使用 PC
(程序计数器)相对寻址方式。处理器使用 PC 相对寻址方式, 分支指令的编码会更简洁, 同时这样也能允许代码从内存的一部分复制到另一部分而不需要更新所有的分支目标地 址。因为我们更关心描述的简单性, 所以就使用了绝对寻址方式。同 I A 32 一样, 所有整数采用小端法编码。当指令按照反汇编格式书写时,这些字节就以相反的顺序出现。
例如,用 十六 进制来 表示指令r mmov q %rsp, Ox123456789abcd ( %r d x ) 的 字节编码。从图 4- 2 我们可以 看到,r mmo vq 的第一个字节为 4 0。源 寄存器%r s p 应该编码放在 rA 字段中, 而基址寄存器%r d x 应该编码放在 rB 字段中。根据图 4- 4 中的寄存器编号,我们得到寄存器指示符 字 节 42 。 最后, 偏 移 量 编码放 在 8 字 节 的 常 数 字 中。首先在Ox l 2 3 45 678 9a b c d 的前面填充 上 0 变成 8 个字节 , 变成字节序列 00 01 23 45 67 89 ab c d。写成按字节反序就是 c d ab 89 67 45 23 01 00 。将它们都连接起来就得到指令的编码40 4 2c d a b 8 9 67 45 23 01 00。
指令集的一个重要性质就是字节编码必须有唯一的解释。任意一个字节序列要么是一个唯一的指令序列的编码, 要么就不是一个合法的字节序列。Y86-64 就具有这个性质, 因为每条指令的第一个字节有唯一的代码和功能组合,给定这个字节,我们就可以决定所有其他附加字节的长度和含义。这个性质保证了处理器可以无二义性地执行目标代码程 序。即使代码嵌入在程序的其他字节中,只要从序列的第一个字节开始处理,我们仍然可以很容易地确定指令序列。反过来说,如果不知道一段代码序列的起始位置,我们就不能准确地确定怎样将序列划分成单独的指令。对于试图直接从目标代码字节序列中抽取出机器级程序的反汇编程序和其他一些工具来说,这就带来了问题。
讫 练习题 4 . 1 确定 下面的 Y 8 6-64 指令 序列的 字节编码。 " . p a s Ox l OO" 那一行 表明这段目标 代码 的起 始地 址应该 是 Ox l OO 。
.pos Ox100 # Start code at address Ox100 irmovq $15,%rbx
rrmovq %rbx,%rcx loop:
rmmovq %rcx,-3(%rbx) addq %rbx,%rcx
jmp loop
芦 练习题 4. 2 确定 下列 每个字 节 序列 所编码的 Y8 6-6 4 指令 序 列。 如果序 列 中有 不合法的字节,指出指令序列中不合法值出现的位置。每个序列都先给出了起始地址,冒 号, 然后是 字节序列 。
A. Ox100: 30f3fcffffffffffffff40630008000000000000
B. Ox200: a06f800c020000000000000030f30a0000000000000090
C. Ox300: 5054070000000000000010f0b01f
D. Ox400: 611373000400000000000000
E. Ox500: 6362a0f0
日日比较 x86-6 4 和 Y86-64 的指令编码
同 x8 6-64 中的 指令编码相比 , Y 8 6- 64 的编码 简单得多, 但是没那么 紧凑 。在所有: 的 Y 8 6-64 指令中, 寄存 器宇段 的位 置都 是固定的 , 而在不 同的 x8 6-64 指令中, 它们的位置是 不一 样的。x8 6- 64 可以将常数值编码 成 1 、2 、4 或 8 个宇 节 , 而 Y 8 6- 64 总是将i 常数值编码成 8 个字 节。
BJ R IS C 和 CISC 指 令 集
x86-64 有 时称 为 “ 复 杂指令 集计 算机" (CISC, 读作 " sisk" ) , 与“精简指令集计算机" (RISC, 读作 " risk" ) 相对。从历 史上看, 先 出现 了 CISC 机 器, 它从 最早的 计算机演化 而来。到 20 世 纪 80 年代早期 , 随 着机 器设 计者加入 了很 多 新 指 令 来 支持 高级 任务(例如 处理循环缓冲区 ,执 行 十进制数计算, 以 及 求 多 项式的值), 大型机和 小型机的指令集 已经 变得 非常 庞 大了。 最 早 的 微 处 理 器 出现 在 20 世 纪 70 年代 早期 , 因 为 当 时的集成电路技 术极 大地制约了一块 芯 片 上 能 实现 些什 么 ,所 以 它们 的 指 令 集非 常有限。微处理 器发 展 得 很 快, 到 20 世 纪 80 年 代早期 , 大型机和小型机的指令 集复 杂度一直都在增加。 x86 家族 沿 着这条道路发展到 IA32 , 最 近 是 x86-64。 即 使 是 x86 系列 也仍 然在不断地变化,基于新出现的应用的需要,增加新的指令类。
20 世 纪 80 年 代 早 期 , RISC 的设 计理念是作为 上 述 发 展 趋 势 的 一种替代而发 展 起 来
的。IBM 的一组硬件和编译 器专 家受到 IBM 研 究 员 John Cocke 的很 大影响 , 认 为他 们可以为更 简单的指令 集形式产生 高效 的 代 码 。 实际上, 许 多 加 到指令集中的 高级 指 令 很难被编译 器产 生, 所以也很 少被 用到。一个较为 简 单的指令 集 可以 用很 少的 硬 件 实现 , 能以 高效 的 流水线结构组织起 来, 类似 于本章 后 面描 述 的 情 况。 直到 多 年 以 后 IBM 才将这个理 念商品 化 , 开发 出 了 Power 和 PowerPC ISA。
加 州大学伯 克利分校的 David Pat terson 和斯坦福 大学的 John Henness y 进 一 步发展
了 RISC 的概念。Pat terson 将 这 种 新 的 机 器 类型 命 名 为 RISC, 而将以前的那种称为
CISC, 因为以 前 没 有 必 要 给 一种几乎是通用的 指 令 集格 式起 名宇。
比较 CISC 和最初的 RISC 指令 集, 我 们发现下面这些一般特性。
CISC 早期的 R ISC
指令数品 很多。Intel 描述全套指令的文档[ 51] 有 指令数量少得多。通常 少于 100 个。
1200多页。
有些指令的延迟很长。包括将一个整块从内存的 一个 没有较长延迟的指令。有些早 期的 RISC 机器甚至没部分复制到另一部分的指令,以及其他一些将多个寄存 有整数乘法指令,要求编译器通过一系列加法来实现器的值复制到内存或从内存复制到多个寄存器的指令。 乘法。
编码是可变长度 的。x86-64 的指令长度可以 是 l ~ 编码是 固定长度的。通常所有的指令都编码为 4 个
15个字节。 字节。
指定操作数的方式 很多 样。在 x86-64 中 ,内 存操作 简单寻址方式。通常只有基址和偏移抵寻址。数指示符可以有许多不同的组合,这些组合由偏移量、
基址和变址寄存器以及伸缩因子组成。
可以对内存和寄存器操作数进行算术和逻辑运算。 只能对寄存器操作数进行算术和逻辑运算。允许使用
内存引用的只有 load 和 store 指令, load 是从内存读到寄存器,store 是从寄存器写到内 存。这种方法被称为load/ store 体系结构。
对机器级程序 来说实现细节是 不可见的。ISA 提 供 对机器级程序来说 实现细节是可 见的。 有些 RISC 机了程序和如何执行程序之间的清晰的抽象。 器禁止某些特殊的指令序列,而有些跳转要到下一条指
令执行完了以后才会生效 。编译器必须在这些约束条件
下进行性能优化。
有条件码。作为指令执行的副产品,设置了一些特 没有条件码。相反,对条件检测来说,要用明确的测试殊的标志位,可以用于条件分支检测。 指令,这些指令会将测试结果放在一个普通的寄存器中。
栈密集的过程链接。栈被用来存取过程参数和返回 寄存器密集的过程链接 。寄存器被用来存取 过程参数地址。 和返回地址。因此有些过程能完全避免内存引用。通常
处理器有更多的(最多的有32 个)寄存器。
Y86-64 指令 集既 有 CISC 指令集的 属性, 也 有 RISC 指令集的 属性。和 CISC 一样, 它有 条件码、长度 可 变的指令, 并用 栈来保 存返回地址。和 RISC 一样的是, 它 采用load / sto re 体 系结 构和规则编码, 通过寄存器来传递过程参数。Y86-64 指 令 集可以看成是 采 用 CISC 指令集( x86) , 但 又根 据 某些 RISC 的原理进行了 简化 。
田日R IS C 与 CIS C 之争
20 世纪 80 年代, 计算机体 系结 构领域里关 于 RISC 指令集和 CISC 指令集优 缺点的争 论 十分激烈。RISC 的支持者声 称在给定硬 件数量的情况下, 通过结合 简 约 式指令集设计、高级编译器技术和流水线化的处理器实现,他们能够得到更强的计算能力。而
CISC的拥定反驳说要 完成 一个给定的任务只需要用较 少的 CISC 指令, 所以他们的机器能够获得更高的 总体性能。
大多数 公 司 都 推 出 了 RISC 处理 器 系 列 产 品 , 包括 S un Microsystems ( SPARC ) 、
IBM 和 Moto rola( PowerPC) , 以及 D屯ital Equipment Corporation ( Alpha) 。一 家英国公 #
司 Acorn Computers Ltd. 提出 了 自 己的 体 系 结构一—-ARM ( 最 开 始是 " Acorn RISC
Machine" 的首宇母缩写), 广泛应用在 嵌入式 系统中(比如手机)。
20 世纪 90 年代早期, 争 论逐 渐平息, 因 为 事 实已 经很 清楚了 , 无论是单纯的 RISC 还是 单纯的 CISC 都不如 结合两者 思想精华的设计。RISC 机器发 展 进 化的过程中, 引入了更多的指令, 而许 多这样的指令都需要执行 多 个周期。今天的 RISC 机器的 指令表中有 几百条指令 , 几乎与 “ 精 简指令集机 器” 的名称不相符了。 那种将实现细节暴露给机器级程序的思想已经被证明是目光短浅的。随着使用更加高级硬件结构的新处理器模型 的开发,许多实现细节已经变得很落后了,但它们仍然是指令集的一部分。不过,作为
RISC 设计的核心的指令集仍然是非常适合在流水线化的机器上 执 行的。
比较新的 CISC 机器也 利 用 了 高性 能流水线结构。就像我们将在 5. 7 节 中讨论的那样 , 它们读取 CISC 指令, 并动 态地翻译成比较 简 单的、像 RISC 那样的操作的序列。例如,一条将寄存器和内存相加的指令被翻译成三个操作:一个是读原始的内存值,一 个是执行加 法运 算, 第 三就是将和写回 内存 。由于动态翻 译通常可以在 实际 指令执行前进行,处理器仍然可以保持很高的执行速率。
除了技 术因素 以外, 市场 因素也在决定不 同指 令 集是 否成功 中起 了很 重要的作用。通过保持与 现 有 处理 器的 兼容性, In tel 以及 x86 使得从 一代处理 器迁移到下一代变得很 容 易。 由 于集成 电路 技术的进步, In t el 和其他 x86 处理 器制造 商能够克服原来 8086 指令集设计造成的低效率,使 用 RISC 技 术产 生出 与 最好的 RISC 机 器相 当的性能。正如 我们在笫 3. 1 节中看到的那样 , I A32 发 展 演 变到 x86-64 提 供 了 一个机会,使 得能够将 RISC 的一些特性结合到 x86 中。在桌面、 便 携 计 算机 和基于服务 器的 计 算领域里,
x86 已经占据 了 完全 的统治地 位。
RISC 处理 器在 嵌入 式处理器市场上表现得非 常出 色,嵌 入式处理 器 负 责控制移动电话、汽车刹车以 及 因特 网电 器等 系统 。 在 这些应用 中,降 低 成本和功耗比保持后向兼容 性 更重要。就出售的处理器数 量来说, 这是个非常广阔而迅 速 成长着的 市场。
4. 1. 4 Y86-64 异 常 #
对 Y86-64 来说, 程序员可见的状态(图4-1 ) 包 括 状 态 码 St a t , 它描述程序执行的总体状态。这个代码可能的值如图 4-5 所示。代码值 1, 命名为 AOK, 表示程序执行正常,
而其他 一些代码则 表示发生了 某种类型的异常。 代码 2 , 命名为 HLT , 表示处理器执行了一条 ha lt 指令。代码 3’ 命名为 ADR , 表示处理器试图从一个非法内存地址读或者向一个非法内存地址写,可能是当取指令的时候,也
可能是当读或者写数据的时候。我们会限制最大的地址(确切的限定值因实现而异),任何访问超出这个 限定值的地址都会引发 ADR 异常。代码
4, 命名为 I NS , 表示遇到了非法的指令代码。
对于 Y8 6- 64 , 当遇到这些异常的时候,我 图 l - ;i
们就简单地让处理器停止执行指令。在更完整的设计中,处理器通常会调用一个异常处理程序
Y86-64 状 态码。在我 们的设 计中, 任何 AOK 以 外的代码都会使处理器停止
(exception handler), 这个过程被指定 用来处理遇到的某种类型的异常。就像在第 8 章中讲述的,异 常处理程序可以被配置成不同的结果, 例如, 中止程序或 者调用一个用户自定义的信号 处理程序 ( s ig n a l h a nd le r ) 。
4. 1. 5 Y86-64 程 序
图 4- 6 给出了下 面这个 C 函数的 x8 6- 6 4 和 Y 86- 6 4 汇编代码 :
long sum(long *start, long | count) | |
---|---|---|
2 | { | |
3 | long sum = O; | |
4 | while (count) { | |
5 | sum += *start,· | |
6 | start++; | |
7 | c oun t - - ; | |
8 | } | |
9 | return sum· | |
10 | } |
x86-64 code Y86-64 code
long sum(long *s t ar t , long count) | long sum(long•start, long count) | ||
---|---|---|---|
start in %rdi, count in %rsi | start in %rdi, count in %rsi | ||
1 | sum: | 1 | sum: |
2 | movl $0, %eax sum= 0 | 2 | irmovq $8,%r8 Constant 8 |
3 | jmp .L2 Goto test | 3 | irmovq $1,%r9 Constant 1 |
4 | .L3: loop: | 4 | xorq %rax,%rax sum= 0 |
5 | addq (%rdi), %rax Add *start to sum | 5 | andq %rsi,%rsi Set CC |
6 | addq $8, %rdi start++ | 6 | jmp test Goto test |
7 | subq $1, %rsi count– | 7 | loop: |
8 | .L2: test: | 8 | mrmovq (%rdi),壮10 Get *start |
9 | testq %rsi, %rsi Test sum | 9 | addq %r10,%rax Add to sum |
10 | jne .L3 If 1=0, goto loop | 10 | addq 壮 8 , %r di start++ |
11 | rep; ret Return | 11 | subq %r9,%rsi count–. Set CC |
12 | test: | ||
13 | jne loop Stop when 0 | ||
14 | ret Return |
图 4-6 Y86-64 汇编程序与 x86-64 汇编程序比较。 Sum 函数计算一个整数 数组的和。
Y86-64 代码与 x86-64 代码遵循了相同的通用模式
x8 6- 64 代码是由 GCC 编译器产生的 。Y8 6- 64 代码与之类似, 但有以下不同点:
Y 8 6- 6 4 将常数加载到寄存 器(第2 3 行), 因为它在算术指令中不能使用立即数。
要实现从内存读取一个数值并将其与一个寄存器相加, Y8 6-64 代码需要两条指令
(第 8 9 行),而x8 6- 64 只需要一条 a d d q 指令(第5 行)。
我们手工编写的 Y86-6 4 实现有一个优势, 即 s ub q 指令(第11 行)同时还设 置了条件码, 因此 GCC 生成代码中的 t e s t q 指令(第9 行)就不是必需的。不过为此, Y8 6-6 4 代码必须 用 a nd q 指令(第5 行)在进入循环之前设 置条件码。
图 4- 7 给出了用 Y8 6-64 汇编代码编写的一个完整的程序文件的例子。这个程序既包括数据,也 包括指令。伪指令( direct ive ) 指明应该将代码或数据放在什么位置, 以及如何对齐。这个程序详细说明了栈的放置、数据初始化、程序初始化和程序结束等问题。
# Execution begins at address 0
2 .pos 0
irmovq stack, %rsp call main
#Setup stack pointer
# Execute main program
7 | # Array | halt # Terminate program of 4 elements |
---|---|---|
8 | .align 8 | |
9 | array: | |
10 | .quad OxOOOdOOOdOOOd | |
11 | .quad OxOOcOOOcOOOcO | |
12 | .quad OxObOOObOOObOO | |
13 14 15 | main: | .quad OxaOOOaOOOaOOO |
16 | irmovq array,%rdi | |
17 | irmovq $4,%rsi | |
18 | call sum # sum(array, 4) | |
19 | ret | |
20 | ||
21 | # long | sum(long *start, long count) |
22 | # start | in %rdi, count in %rsi |
23 | sum: | |
24 | irmovq $8,%r8 # Constant 8 | |
25 | irmovq $1, %r9 # Constant 1 | |
26 | xorq %rax,%rax #sum= 0 | |
27 | andq %rsi,%rsi # Set CC | |
28 | jmp test # Goto test | |
29 | loop: | |
30 | mrmovq (%rdi),%r10 # Get *start | |
31 | addq %r10,%rax # Add to sum | |
32 | addq %r8,%rdi # start++ | |
33 | subq %r9,%rsi # count–. Set CC | |
34 | test: | |
35 | jne loop # Stop when 0 | |
36 | ret # Return | |
37 | ||
38 | # Stack | starts here and grows to lower addresses |
39 | . pos Ox200 | |
40 | stack: |
图4-7 用 Y86-64 汇编代码编写的 一个例子程 序。调用 s um 函数来计算一个具有 4 个元素的数组的和
在这个程序中, 以 “. " 开头的词是汇编器伪 指令( a s s e m b le r directives), 它们告诉汇编器调整地址,以 便 在 那 儿 产 生 代 码或插入一些数据。伪指令 . p o s O( 第 2 行)告诉汇编器应该从 地址 0 处开始产生代码。这个地址是所有 Y 86- 64 程序的起点。接下来的一条指令
(第3 行)初始化栈指针。我们可以看到程序结尾处(第40 行)声明了标号 s t a c k , 并且用一个 . p o s 伪指令(第3 9 行)指明地址 Ox 2 0 0 。 因 此栈会从这个地址开始,向 低 地 址 增 长 。我们必须 保证栈不会增长得太大以至于覆盖了代码或者其他程序数据。
程序的 第 8 ~ 1 3 行声明了一个 4 个字的数组,值 分 别 为
Ox OOOd OOOd OOOd OOOd , Ox OOc OOOc OOOc OO Oc O OxObOOObOOObOOObOO, Ox a OOOa OOOa OOOa OOO
标号 a r r a y 表明了这个数组的起始,并 且 在 8 字节边界处对齐(用.a li g n 伪 指令指定)。
第 16~ 19行给出了 " ma i n” 过程,在过 程中对那个四字数组调用了 s um 函数 ,然 后停 止 。正如例子所示,由 千我们创建 Y8 6- 64 代码的唯一工具是汇编器, 程序员必须执行本
来通常交 给编译器、链接器和运行时系统来完成的任务。幸好我们只用 Y 8 6- 6 4 来写一些小的程序,对 此一些简单的机制就足够了。
图 4-8 是 Y A S 的汇编器对图 4- 7 中代码进行 汇编的结果。为了便于理解,汇 编 器的输出结果是 ASCII 码格式。汇编文件中有指令或数据的行上,目 标 代码包含一个地址,后 面跟着 1 ~ 1 0 个 字 节的 值 。
我们 实现了一个指令集模 拟器,称 为 Y IS , 它的目的是模拟 Y 8 6- 6 4 机器代码程序的执行, 而不用试图去模拟任何具体处理器实现的行为。这种形式的模拟有助于在有实际硬件可用 之前调试程序,也 有 助于检查模拟硬件或者在硬件上运行程序的结果。用 Y IS 运行例子的 目标代码, 产 生如下输出:
Stopped in 34 steps at PC= Ox13 . Status’HLT’, CC Z=l S=O O=O Changes to r egi st er s :
%rax: OxOOOOOOOOOOOOOOOO
%rsp: OxOOOOOOOOOOOOOOOO
%rdi: OxOOOOOOOOOOOOOOOO
%r8: OxOOOOOOOOOOOOOOOO
%r9: OxOOOOOOOOOOOOOOOO
%r10: OxOOOOOOOOOOOOOOOO
Changes to memory:
Ox01f0: OxOOOOOOOOOOOOOOOO Ox01f 8 : OxOOOOOOOOOOOOOOOO
OxOOOOabcdabcdabcd Ox0000000000000200 Ox0000000000000038 Ox0000000000000008 Ox0000000000000001 OxOOOOaOOOaOOOaOOO
Ox0000000000000055 Ox0000000000000013
模拟输出的第一行总结了执行以及 PC 和程序状态的结果值。模拟器只打印出在模拟 过程中 被改变了的寄存器或内存中的字。左边是原 始值(这里都是 0 )’ 右 边是最终的值。从输出中 我们可以看到, 寄 存 器 %r a x 的 值 为 Ox a b c d a b c d a b c d a b c d , 即 传 给子函数 s u m 的四元素数组的和。另外, 我们还能看到栈从地址 Ox 2 0 0 开始,向 下 增 长 ,栈 的 使 用 导 致内存地址 Ox lf O Ox lf 8 发 生了变化。可执行代码的最大地址为 Ox 0 9 0 , 所以数值的入栈和出栈不 会破坏可执行代码。
练习题 4. 3 机器级程序 中 常见的模式之一是 将一个常 数值 与 一个寄存器相加。利用目前 已 给 出 的 Y 8 6- 6 4 指令, 实 现这个操作需 要 一条 ir mo v q 指令把 常数加载 到 寄存器, 然后 一条 a d d q 指令把这个寄存器值 与 目标 寄存器值相加。假设我 们 想增加一条新指 令 i a d d q , 格式如下:
字节 0 1 2 5 6
iaddq V, rB I c I O I F IrB I V
该指令 将常数值 V 与 寄存 器 rB 相加。
使用 i a d d q 指令 重写 图 4- 6 的 Y 8 6- 64 s u m 函 数。 在 之前 的代码 中, 我们 用寄存器%r 8 和%r 9 来保 存常数值。 现在 ,我们 完全 可以 避免使用 这些寄 存器。
OxOOO:
# Execution begins at address 0
.pos 0
OxOOO: 30f40002000000000000 OxOOa: 803800000000000000
Ox013: 00
Ox018:
Ox018:
Ox018: OdOOOdOOOdOOOOOO Ox020: cOOOcOOOcOOOOOOO Ox028: OOObOOObOOObOOOO Ox030: OOaOOOaOOOaOOOOO
Ox038:
Ox038: 30f71800000000000000 Ox042: 30£60400000000000000 Ox04c: 805600000000000000
Ox055: 90
irmovq stack, %rsp call main
halt
# Array of 4 elements
.align 8
array:
.quadOxOOOdOOOdOOOd
.quad OxOOcOOOcOOOcO
.quad OxObOOObOOObOO
.quad OxaOOOaOOOaOOO
main:
irmovq array,%rdi irmovq $4,%rsi call sum
ret
#Setup stack pointer
# Execute main program
# Terminate program
# sum(array, 4)
# long sum(long *start, long count)
Ox056:
# start in %rdi, count in 1r儿 s i sum:
Ox056: 30f80800000000000000 Ox060: 30f90100000000000000 Ox06a: 6300
Ox06c: 6266
Ox06e: 708700000000000000 Ox077:
irmovq $8,%r8 irmovq $1,%r9 xorq %rax,%rax andq %rsi,%rsi jmp test
loop:
# Constant 8
# Constant 1
#sum= 0
# Set CC
# Goto test
Ox200: Ox200:
# Stack starts here and grows to lower addresses
.pos Ox200 stack:
图4-8 YAS 汇编器的输出 。每一行包含一个十六进制的地址 , 以及字节数在 1~ 10 之间的目标代码
匹 练习题 4. 4 根据下面的 C 代码 , 用 Y86- 64 代码来实现一个 递 归 求和函 数 r s um:
long rsum(long *start, long count)
{
if (count <= 0) return O;
return *start+ rsum(start+l, count-1);
}
使用 与 x86- 64 代码相同的参数传递和寄存器保存 方 法。 在 一 台 x86- 64 机器上编
译这 段 C 代码, 然后再把那些指 令翻译成 Y86- 64 的指令, 这样做可 能会很有帮助 。练习题 4. 5 修 改 s um 函数的 Y8 6- 64 代码(图 4-6) , 实现函 数 a b s Sum, 它计算一个数组的绝对值的和。在内循环中使用条件跳转指令。
练习题 4. 6 修改 s um 函 数的 Y8 6- 64 代码(图 4- 6 ) , 实 现函 数 a b s Sum,
数组的绝对值的和。在内循环中使用条件传送指令。
它计算一个
1. 6 一 些 Y86 -6 4 指 令的 详情
大多数 Y8 6- 64 指令是以一种直接明了的方式修改程序状态的,所 以 定 义 每条指令想要达到的结果并不困难。不过,两个特别的指令的组合需要特别注意一下。
pus hq 指令会把栈指针减 8 , 并且将一个寄存器值写入内存中。因此, 当 执 行 p u s h q
%rs p 指令时 ,处 理 器 的 行 为是不确定的, 因 为要人栈的寄存器会被同一条指令修改。通常有两种不同的约定: 1 ) 压入%r s p 的 原 始 值 , 2 ) 压入减去 8 的%r s p 的 值 。
对千 Y86- 64 处理器来说, 我们采用和 x86-64 一样的做法, 就 像 下 面这个练习题确定出的那样。
练习题 4. 7 确定 x86-64 处理器上指令 pus hq %r s p 的行为。 我们 可以通过阅读 In t el 关于这条指令的文档来了解它们的做法,但更简单的方法是在实际的机器上做个实 验。 C 编译器正常情况下是不会 产生这 条指令的, 所以 我们 必须用 手 工 生 成的 汇编 代码 来完成 这一任务。下面是我 们 写 的 一个测 试程 序(网 络旁 注 ASM : EAS M , 描绘如何编 写 C 代码和手写 汇编 代码结合的程序):
.text
.globl pushtest pushtest:
movq | %rsp, | %rax | Copy stack pointer |
---|---|---|---|
pushq | %rsp | Push stack pointer | |
popq | %rdx | Pop it back | |
subq | %rdx, | %rax | Return O or 8 |
ret |
在实验中 , 我们发现函数 p u s h 七e s t 总是 返回 o, 这表 示在 x86- 64 中 p u s h q %rsp
指令的行为是怎样的呢?
对 po pq %r s p 指 令 也 有 类 似的歧义。可以将%r s p 置为从内存中读出的值, 也 可 以 置为加了增量 后的栈指针。同 练习题 4. 7 一样, 让 我们做个实验来确定 x86-64 机器是怎么处理这条指 令的 ,然 后 Y86- 64 机器就采用同样的方法。
练习题 4. 8 下面这个汇编 函 数让 我们确定 x86-64 上指 令 popq %r s p 的行为 :
. t ext
.globl poptest poptest:
4 movq %rsp, r% di Save stack pointer
5 pushq $0xabcd Push test value
6 popq %r s p Pop to stack pointer
7 movq 。r1/. s p, %rax Set popped value as return value
8 movq
9 ret
r% di , %rsp Restore stack pointer
我们发现函数总是返回 Oxa b c d 。 这表 示 po pq %r s p 的行为 是怎样的? 还有什 么
其他 Y86-64 指令也会有相同的行为吗?
日 日 正 确了解细节: x86 模型间的 不一致
练习题 4. 7 和练 习题 4. 8 可以 帮 助我们确定对于压入和弹出 栈 指 针指令的 一致惯例。看上去似乎没有理由会执行这样两种 操 作, 那么一个很 自 然的 问题就是“ 为什 么要担心这样一些吹毛求疵的细节呢?”
从下面 In tel 关 于 P US H 指令的文档[ 51] 的节 选中, 可以 学到 关 于这个一致的重要性的有用的 教训:
对于 IA-32 处理器,从 Inte l 286 开始 , P US H ESP 指令将 ESP 寄存 器的 值压入栈
中,就 好 像 它存 在于这 条指令被执行之前。(对于 Intel 64 体 系结构、IA-32 体系结 构的实地 址模式和虚 8086 模 式 来说 也 是这样。)对于 I ntel ® 8 086 处理 器, P US H SP 将 SP寄存器的 新值压入栈中(也就是减去 2 之后的值)。( P US H ESP 指令。Intel 公 司。50 。)虽然这个说明的具体细节可能难以理解,但是我们可以看到这条注释说明的是当执
行压入栈指针寄存器指 令时, 不同型号的 x86 处理器会 做 不同的事情。有些会压入原始的值,而有些会压入减去后的值。(有趣的是,对于弹出栈指针寄存器没有类似的歧 义。)这种不一致有两个缺 点:
- 它降 低 了代 码 的可移植 性。取 决于处理器模 型, 程序可能会有不 同的行为。 虽 然这样特殊的指令并不常见,但 是 即 使 是 潜在的不兼容也可能带 来严 重的后果。
它增加了文档的复杂性。正如在这里我 们看到的那样 , 需要一个特别的说明来澄清这些不同之 处。即使没有这样的特殊情况, x86 文档就已经够复杂的 了。
因此我们的结论是,从长远来看,提前了解细节,力争保持完全的一致能够节省很多的麻烦。
4. 2 逻辑设计和硬件控制语言 HCL
在硬件设计中, 用电 子电路来计算对位进行运算的函数,以 及 在 各 种 存 储 器 单 元 中存储 位 。 大 多 数 现代电路技术都是用信号线上的高电压或低电压来表示不同的位值。在当前的技术 中 , 逻辑 1 是用 1. 0 伏特左右的高电压表示的 ,而 逻辑 0 是用 0. 0 伏特左右的低电压表示的。要实现一个数字系统需要三个主要的组成部分:计 算 对 位 进行操作的函数的组合 逻辑、存储位的存储器单元,以 及 控 制 存 储 器单元更新的时钟信号。
本节简要描述这些不同的组成部分。我们还将 介绍 HCL ( Hardware Cont rol Lan
guage, 硬件控制语言),用这种语言来描述不同处理器设计的控制逻辑。在此我们只是简略地描述 HCL , H CL 完整的参考请见网络旁注 ARC H : H CL 。
日 日 现代逻辑设计
曾经,硬件设计者通过描绘示意性的逻辑电路图来进行电路设计(最早是用纸和笔, 后来是用计 算机图形终 端)。现在, 大多数设 计都是用硬件描 述语言( H ard ware Description
Language, H DL) 来表达的。H DL 是一种 文本表 示, 看上去和编程语言类似, 但 是 它是用来描 述硬件结构而不是 程序行为的。最常用的 语言是 Verilog , 它的 语法类似于 C ; 另一 种是 V H DL , 它的 语法类似 于编程语言 Ada。这些语 言本来都是 用 来表 示数 字电 路的模拟 模型的 。20 世 纪 80 年代中期,研 究者开发 出 了 逻 辑合成 (l ogic synt hes is ) 程序, 它可 以根据 H DL 的描述生成 有效的电路设 计。现在有许多 商用的 合成程序, 已经成为产生 数宇电路 的主要技术。从手工设 计电路到合成生成 的转变就 好 像 从 写 汇编程序到 写 高级语言程序, 再 用编 译器来 产 生机 器代码的转变一 样。
我们的 HCL 语言只表达硬件设计的控制部分, 只有有限的操作集合 ,也 没有模块化。不过, 正如我们会看到的 那样,控 制逻辑是设计微处理器中 最难的部分。我们已经开发出 了将 HCL 直接翻译成 Verilog 的工具 , 将这个代码与基本硬件单元的 Verilog 代码结合起来, 就能产生 H DL 描述,根 据 这个 H DL 描述就可以合成实际能 够工作的 微处理器。通过小心地分离、设计和测试控制逻辑,再加上适当的努力,我们就能创建出一个可以工作的微 处理器。 网络 旁注 ARCH : VLOG 描述了如何 能产生 Y86-64 处理器的 Verilog 版本。
4. 2. 1 逻辑门
逻辑门 是数字电路的基本计算单元。它们产生的输出, 等 千它们输入位值的某个布尔函数。图 4-9 是 布尔函数 AND 、OR 和 NO T 的标 准符号, C 语 言 中 运算符 ( 2. 1. 8 节)的逻辑门下面是对应的 HCL 表达式: A ND 用 && 表示, O R 用 1 1 表示, 而 NOT 用! 表
示。我 们用这些符号而不用 C 语言中的位运算符 &、 I 和~ , 这是因为逻辑门只对单个
位的数进行 操作,而 不 是 整个字。虽然图中只说明了 AND 和 OR 门的 两个输入的版本, 但是常 见的是它们作为 n 路操作, n> 2。不过,在 H CL 中 我们还是把它们写作二元运算符, 所以 , 三个输入的 AND 门 ,输 入为 a 、 And OR NOT
b 和 c , 用 H CL 表示就是 a &&b &&c 。
逻辑门 总是活动的 ( active ) 。一旦 一个门的输入变化了,在很短的时间内,输出就会
:0-out
- 输出=a&&b
D- out a 令 o- out
输出=a l lb 输出=!a
相应地 变化。
- 2. 2 组 合 电 路 和 HCL 布 尔 表 达 式
图 4- 9 逻辑门类型。每个门产生的输出
等于它输入的某个布尔函数
将很多 的逻辑门组合成一个网,就 能 构 建 计 算 块( computa tional block), 称为组合电
路( combinational circ uits ) 。如何构建这些网有几个限制:
·每个逻辑门的输入必须连接到下述选项之一: 1 ) 一个系统输入(称为主输入), 2 ) 某个存储器单元的输出, 3 ) 某 个 逻辑门的输出。
两个或多个逻辑门的输出不能连接在一起。否则它们可能会使线上的信号矛盾, 可能会导致一个不合法的电压或电路故障。
·这个网必须是无环的。也就是在网中不能有路径经过一系列的门而形成一个回路, 这样的回路会导致该网络计算的函数有歧义。
图 4-10 是一个 我们觉得非常有用的简单组合电路的例子。它有两个输入 a 和 b , 有唯一的输 出 eq , 当 a 和 b 都是 1 ( 从上面的 AND 门可以看出)或都是 0 ( 从下面的 AND 门可以看出)时, 输出为 1。用 HCL 来写这个网的函数就是:
bool eq = (a && b) 11 (!a && !b);
这段代码简单 地定义了位级(数据类型 b o o l 表明了这一点)信号e q , 它是输入 a 和 b 的函数。从这个例子可以看出 HCL 使用了 C 语言风格的语法,`= '将一个信号名与一个表达式联系起来。不过同 C 不一样, 我们不把它看成执行了一次计算并将结果放入内存中某个位置。相反,它只是给表达式一个名字。
比囡 练习题 4. 9 写 出信 号 xor 的 HCL 表达 式, x o r 就是 异或 , 输入 为 a 和 b。信号 xo r
和上面定 义的 e q 有什 么关 系?
图 4-11 给出了另一个简单但很有用的 组合电路, 称为多路复用 器 ( m ultiplexo r , 通常称为 " M U X" ) 。多路复用器根据输入控制信号的值,从 一组不同的数据信号中选出一个。在这个单个位的多路复用器中, 两个数据信号是输入位 a 和 b , 控制信号是 输入位 s 。当 s 为 1 时, 输出等于 a ; 而当 s 为 0 时, 输出等于 b。在这个电路中, 我们可以看出两个A N D 门决定了是否将它们相对应的数据输入传送到 OR 门。当 s 为 0 时, 上面的 AND 门 将传送信号 b( 因为这个门的另一个输入是!s )’ 而当 s 为 1 时,下 面的 AND 门将传送信号 a 。接下来,我 们来写输出信号的 HCL 表达式 , 使用的就是组合逻辑中相同的 操作:
bool out= (s && a) I I (!s && b);
a
e q
OU 七
图 4-10 检测位相等的组 合电路。当输入都为 0 图 4- 11 单个位的多路复用器电路。如果控制信号或都为 1 时, 输出等于 1 s 为 1 . 则 输出 等 千输 入 a ; 当 s 为 0
时 ,输 出 等 于 输 入 b
H CL 表达式很清楚地表明了组合逻辑电路和 C 语言中逻辑表达式的对应之处。它们都是用布尔操作来对输入进行计算的函数。值得注意的是,这两种表达计算的方法之间有 以下区别:
因为组合电路是由一系列的逻辑门组成, 它的属性是输出会持续地响应输入的变化。如果电路的输入变化了,在一定的延迟之后,输出也会相应地变化。相比之 下, C 表达式只 会在程序执行 过程中被 遇到时才进行 求值。
C 的逻辑表达式允 许参数是任意整数, 0 表示 F ALSE , 其他任何值都表示 T RUE。而逻辑门 只对位值 0 和 1 进行操作。
C 的逻辑表达式有个属性就是它们可能只被部分求值。如果一个 AND 或 OR 操作的 结果只用对第一个参数求 值就能确定, 那么就不会对第二个参数求值了。例如下面的 C 表达式:
(a && !a) && func(b,c)
这里函数 f u n c 是不会被调用的 , 因为表达式 ( a && ! a ) 求值为 0 。而组合逻辑没有部分求值这条规则,逻辑门只是简单地响应输人的变化。
2. 3 字级的组合电路和 HCL 整数表达式
通过将逻辑门组合成大的网,可以构造出能计算更加复杂函数的组合电路。通常,我 们设计能对数据字( wo rd ) 进行操作的电 路。有一些位级信号, 代表一个 整数或一些控制模
式。例如 , 我们的处 理器设计将包含有很 多字,字的 大小的范围为 4 位到 64 位, 代表整数、地址、指令代码和寄存器标识符。
执行字级计算的组合电路根据输入字的各个位 ,用 逻辑门 来计算输出 字的各个位。例如图 4-12 中的一个组合电路, 它测试两个 64 位字 A 和 B 是否相等。也就是, 当且仅当 A 的每一位都和 B 的相应位相等时, 输出才 为 1。这个电 路是用 64 个图 4-10 中所示的单 个位相等电路实现的。这些单个位电路的输出用一个AND 门连起来 , 形成了这个电路的输出。
:二勹A B
a ) 位级实现 b ) 字级抽象
图 4-12 字级相等测试电路。当字 A 的 每一位与字 B 中相应的位均相等时, 输出等于 1。字级相等是 HCL 中的一个 操作
在 HCL 中, 我们将所有字级的信号都声明为 i n t , 不指定字的大小。这样做是为了简单。在 全功能的硬件描述语言中, 每个 字都可以声明为有特定的位数。HCL 允许比较字是否相等, 因此图 4-12 所示的电 路的函数可以在字级上表达成
bool Eq = (A == B) ;
这里参数 A 和 B 是 i n t 型的。注意我们使用 和 C 语言中一样的语法习惯,'= '表示赋值,而’==‘是相等运算符。
如 图 4-1 2 中右边所示 , 在画字级电路的时候, 我们用中等粗度的线来表示携带字的
每个位的线路,而用虚线来表示布尔信号结果。
练习题 4. 10 假设你用练习题 4. 9 中的异或电路而不是位级的相等电路来实现一个 字级的相等电路。设计一个 64 位字的相等电路需要 64 个字级的异或电路, 另外还要两个逻辑门。
图 4-13 是字级的多路复用器电路。这个电路根据控制输入位 s , 产生一个 64 位的字
Out, 等于两个输入字 A 或者 B 中的一个。这个电路由 64 个相同的 子电路组成, 每个子电路的结构都类似于图 4-11 中的位级多路复用器。不过这个字级的电路并没有简单地复制
64 次位级多 路复用器, 它只产生一次! s , 然后在每个位的地方都重复使用它,从而减少反相器或非门 ( inver t ers )的数量。
处理器中会用到很多种多路复用器,使得我们能根据某些控制条件,从许多源中选出 一个字。 在 HCL 中,多 路复用函数是用情况表 达式 ( cas e ex pres sion ) 来描述的。情况 表达式的通用格式如下:
[ | ||
---|---|---|
select1 select2 | exp r1 ; expr2; | |
selectk | exprk; | |
] |
这个表达式包含一系列的情况, 每种情况 i 都有一个布尔表达式 sel ect ; 和一个整数表达式 ex p r ; , 前者表明什么时候该选择这种情况,后者指明的是得到的值。
s
b 63
a 63
out 63
b 62
out 62
二., …丑..、Out
int Out= [ s A;
1 : B;
b 。
ou t 。
a 。
];
b ) 字级抽象
图 4-13 字级多路复用器电路。当控制信号 s 为 1 时, 输出会等于输人字 A,
否则等于 B。HCL 中用情况( case) 表达式来描述多路复用器
同 C 的 S W工t c h 语句不同,我 们不要求不同的选择表达式之间互斥。从逻辑上讲,这些选择表达式是顺序求值的, 且第一个求值为 1 的情况会被选中。例如, 图 4-13 中的字级多路 复用器用 HCL 来描述就是:
word Out= [
s: A;
1: B;
];
在 这段 代 码 中 ,第 二个选择表达式就是 1, 表明如果前面没有情况被选中,那就选择这种情况。这是 HCL 中一种指定默认情况的方法。几乎所有的情况 表达式都是以此结尾的。
允许不互斥的选择表达式使得 HCL 代码的可读性更好。实际的硬件多路复用器的信号必须互斥,它 们要控制哪个输入字应该被传送到输出,就像 图 4-13 中的信号 s 和 ! s 。要将一个 HCL 情况表达式翻译成硬件, 逻辑合成程序需要分析选择表达式集合,并 解决任何可能的冲突,确保只有第一个满足的情况才会被选中。 s1
选择表达式可以是任意的布尔表达式,可以有任意 s 0
多的情况。这就使得情况表达式能描述带复杂选择标准 D
的、多 种输入信号的块。例如, 考虑图 4-14 中所示的四 B
路复用器的图。这个电路根据控制信号 s l 和 s 0 , 从 4
Out 4
个输入字 A、B、C 和 D 中 选择一个, 将控制信号看作一个两位的二进制数。我们可以用 HCL 来表示这个电路, 用 布尔表达式描述控制位模式的不同组合:
word Ou t 4 = [
!s1 && !s0 : A; # 00
图 4-14 四路复用器。控制信号 s l 和s 0 的不同组合决 定了哪个数据输人会被传送到输出
! s1 : Bi # 01
- ! s0 : C; # 10
D; # 11
];
右边的 注释(任何以#开头到行尾结 束的文字都是注释)表明了 s l 和 s 0 的什么组合会导致该种情况会被选中。可以看到选择表达式有时可以简化,因为只有第一个匹配的情况 才会被选中。例如,第二个表达式可以写成!sl, 而不用写得更完整! s l && s0, 因为另一种可能 s l 等于 0 已经出现在了第一个选择表达式中 了。类似地 , 第三个表达式可以写作
!s0, 而第四个可以简单地写成 1。
来看最 后一个例子, 假设我们想设计一个逻辑电 路来找一组字 A、B 和 C 中的最小值,
;三 贮n 3
用 HCL 来表达就是 :
欢 or d Min3 = [
A<= B && A<= C : A;
- B <= A && B <= C: B;
C;
];
区 }练习题 4. 11 计算 三个 字中最 小值的 H C L 代码包 含了 4 个形如 X< = Y 的比 较表达 式。重写代码计算同样的结果,但只使用三个比较。
练习题 4. 12 写 一个 电 路的 H CL 代码, 对于输入 宇 A、 B 和 C , 选择中间值。也就是, 输出等 于三个输入 中居 于最小值 和最 大值 之间的 那个 字。
组合逻辑电路可以设计成在字级数据上执行许多不同类型的操作。具体的设计已经超
出了我们讨论的范围。算术/逻辑单元 ( AL U )是一种很重要的组 合电路,图 4-15 是它的一个抽象 的图示。这个电路有三个 输入: 标号为 A 和 B 的两个数据输入, 以及一个控制输人。根据控制输入的设置,电路会对数据输入执行不同的算术或逻辑操作。可以看到,这个 ALU 中画的四个操作对应于 Y86-64 指令集支持的四 种不同的整数操作, 而控制值和这 些操作的功能码相对应(图4-3) 。我们还注意到减法的 操作数顺序, 是输入 B 减去输入 A。之所以这样做, 是为了使 这个顺序与 sub q 指令的参数顺序一致。
图 4- 15 算术/逻辑单元 ( ALU) 。根据函数输入的设 置,该 电路会执行四种算术和逻辑运算中的一 种
2. 4 集合关系
在处理器设计中,很多时候都需要将一个信号与许多可能匹配的信号做比较,以此来 检测正在处理的某个指令代码是否属千某一类指令代码。下面来看一个简单的例子, 假设想从一 个两位信号 c o d e 中选择高位和低位来 为图 4-14 中的四路复用器产生信号 s 1 和 s 0 ,
如下图所示:
co de王丑:;
}- Out4
在这个电路中, 两位的信号.cod e 就可以用来控制 对 4 个数据字 A、 B、C 和 D 做选择。根据可能的 c od e 值, 可以用相等测试来表示信号 s l 和 s 0 的产生:
bool s1 =code== 2 II code== 3;
bool s0 = code == 1 11 code == 3;
还有一 种更简洁的方式来表示这样的属性: 当 c o d e 在集合{ 2 , 3 }中时 s 1 为 1, 而
c o d e 在集合{ 1, 3 } 中时 s 0 为 1 :
bool s1 = code in { 2, 3 };
bool s0 = code in { 1, 3 };
判断集 合关系的通用 格式是:
iexpr in {ie 工 Pr1 , iex p r 2 , …,iex p r k }
这里被测试的值 i ex p r 和待匹配的值 ie x p r 1 ~ ie x p r k 都是整数表达式。
2. 5 存储器和时钟
组合电路从本质上讲,不存储任何信息。相反,它们只是简单地响应输入信号,产生等 于输入的某个函数的输出。为了产生时序 电路 ( sequential c订cu it ) , 也就是有状态并且在这个状态上进行计算的系统,我们必须引入按位存储信息的设备。存储设备都是由同一个时钟控制 的,时钟是一个周期性信号,决定什么时候要把新值加载到设备中。考虑两类存储牉设备:
- 时钟寄存 器(简称寄存 器)存储单个位或字。时钟信号控制寄存器加载输入值。
随机访问存储 器(简称内存)存储多 个字,用 地址来选择该读或该写哪个字。随机访问存储器的例子包括: 1 ) 处理器的虚 拟内存系统 , 硬件和操作 系统软件结合起来使处理器可以在一个很大的地址空间内访问任意的字; 2 ) 寄存器文件,在 此,寄存器标识符作为地址 。在 IA32 或 Y86-64 处理器中, 寄存器文件有 15 个程序寄存 器(%
r a x ~ %r l 4) 。
正如我们看 到的那样, 在说到硬 件和机器级编程时 ," 寄存器” 这个词是两个有细微差别的事情。在硬件中 , 寄存器直接将它的输入和输出线连接到电路的其他部分。在机器级 编程中 , 寄存器代表的是 CPU 中为数不多的可寻址的字, 这里的地址是 寄存器 ID。这些字通常都存在寄存器文件中,虽然我们会看到硬件有时可以直接将一个字从一个指令传 送到另一个指令,以避免先写寄存器文件再读出来的延迟。需要避免歧义时,我们会分别 称呼这两类寄存器为“硬件寄存器”和“程序寄存器”。
图 4-1 6 更详细地说明 了一个硬件寄存器以及它是如何工作的。大多数时候, 寄存器都保待在稳定状态(用x 表示), 产生的输出等千它的 当前状态。信号沿着寄存器前面的组合逻辑传播, 这时, 产生了一个新的寄存器输入(用 y 表示), 但只要时钟是低电位的,寄存祥的输出就仍然保持不变。当时钟变成高电位的时候,输入信号就加载到寄存器中,成 为下一个状态 y , 直到下一个时钟上升沿, 这个状态就 一直是寄存器的新输出。关键是寄
- 时钟寄存 器(简称寄存 器)存储单个位或字。时钟信号控制寄存器加载输入值。
存器是作为电路不同部分中的组合逻辑之间的屏障。每当每个时钟到达上升沿时,值才会 从寄存器的 输入传送到输出。我们的 Y86-64 处理器会用时钟寄存器保存程序计数 器(PC)、条件代码 CCC) 和程序状态( S t at ) 。
状态=X 状态=Y
输入- 勹 一 二 输出 Y
图 4-16 寄存器操作。寄存器输出会一直保 持在当前寄存 器状态上 , 直到时 钟信号上升。当时钟上升时,寄存器输人上的值会成为新的寄存器状态
下面的图展示了一个典型的寄存器文件:
读端口
sr cAI A
valB
言 B
寄存器文件 w
valW
d s t W 写端口
时钟
寄存器文件有两个 读端口 CA 和 B)’ 还有一个写 端口 CW) 。这样一个多端口随 机访问存储器允 许同时进行多个读和写操作。图中所示的寄存器文件中, 电路可以读两个程序寄存器的值,同时更新第三个寄存祥的状态。每个端口都有一个地址输入,表明该选择哪个 程序寄存器, 另外还有一个数据输出或对应该程序寄存器的输入值。地址是用图 4-4 中编码表示 的寄存器标识符。两个读端口有地址 输入 sr c A 和 sr c B( " so urce A" 和 " so ur ce B" 的缩写)和数据输出 v a l A 和 v a l B( " va lue A" 和 " va lue B" 的缩写)。写端口有 地址输入dstw(“destination W” 的缩写), 以及数据输入 v a l W( " val ue W" 的缩写)。
虽然寄存器文件不是组合电路,因为它有内部存储。不过,在我们的实现中,从寄存 器文件读数 据就好像它是一个以地址为输入、数据为输出的一个组合逻辑 块。当 s r c A 或s r c B 被设成某个寄存器 ID 时,在 一段延迟之后 , 存储在相应程序寄存器的值就会出现在va l A 或 v a l B 上。例如 , 将 sr c A 设为 3’ 就会读出程序寄存器%r b x 的值, 然后这个值就会出现在输出 va l A 上。
向寄存器文件写入字是由时钟信号控制的,控 制方式类似于将值加载到时钟寄存 器。每次时钟上升时 , 输入 va l W 上的值会被写入输入 ds t W 上的寄存器 ID 指示的程序寄存器。当ds t W 设为特殊的 ID 值 Ox F 时 , 不会写任何程序寄存器。由于寄存 器文件既可以读也 可以写, 一个很自然的间题就是“如果我们试图同时读和写同一个寄存器会发生什么?"答案简单明了: 如果更新一个寄存器,同时在读端口上用同一个寄存器 ID, 我们会看到一个从旧值到新值的变化。当我们把这个寄存器文件加入到处理器设计中,我们保证会考虑到这个属性的。
处理器有一个随机访问存储器来存储程序数 据, 如下图所示:
数据输出
时钟
地址数据输入
这个内存有一个地址输入,一个写的数据输入,以及一个读的数据输出。同寄存器文件 一样, 从内存中读的操作方式类似于组合逻辑 : 如果我们在输入 a ddr e s s 上提供一个地址,
并将 wr i t e 控制信号设置为 o, 那么在经过一些延迟之后,存储在那个地址上的值会出现在
输出 d a 七a 上。如果地址超出了范围 , e rr or 信号 会设置为 1 , 否则就设置为 0。写内存是由时钟控制的: 我们将 a dd r e s s 设置为期望的地址 , 将 da t a i n 设置为期望的值 , 而 wr i t e 设置为 1。然后当我们控制时钟时 ,只 要地址是合法的, 就会更新内存中指定的位置。对于读操作来说, 如果地址是不合法的 , e rr or 信 号会被设置为 1。这个信号是由 组合逻辑产生的 , 因为所需要的边界检查纯粹就是地址输入的函数,不涉及保存任何状态。
囚 日 现实的存储器设计
真实微处理器中的存储器系统比我们在设计中假想的这个简单的存储器要复杂得 多。它是由几种形式的硬件存储器组成的,包括几种随机访问存储器和磁盘,以及管理 这些设备的各种硬件和软件机 制。存储器 系统的设计和特点 在第 6 章中描 述。
不过,我们简单的存储器设计可以用于较小的系统,它提供了更复杂系统的处理器和存储器之间接口的抽象。
我们的处理器还包括另外一个只读存储器,用来读指令。在大多数实际系统中,这两个存储器被合并为一个具有双端口的存储器: 一个用来读指令,另 一个用来读或者写数据。
4. 3 Y86-64 的 顺 序实现
现在已经 有了实现 Y86- 64 处理器所需要的部件。首先, 我们描述一个称为 SE Q ( " se q ue n tia l" 顺序的)的处理器。每个时钟周期 上, S E Q 执行处理一条完整指令所需的所有步骤。不过,这需要一个很长的时钟周期时间,因此时钟周期频率会低到不可接受。我们开 发 SEQ 的目标就是提供实现 最终目的的第一步, 我们的最终目的是实现一个高效的、流水线化的处理器。
3. 1 将处理组织成阶段
通常,处理一条指令包括很多操作。将它们组织成某个特殊的阶段序列,即使指令的 动作差异很大, 但所有的指令都遵循统一 的序列。每一步的具体处理取决于正在执行的指令。创建这样一个框架, 我们就能 够设计一个充分利用硬件的处理器。下面是关 千各个阶段以及各阶段内执行操作的简略描述:
取指( fe tch ) : 取指阶段从内存读取指令字节, 地址为程序计数器( PC) 的值。从指令中抽取出指令指示符字节的两个四位部分, 称为 乓o d e ( 指令代码)和辽u n ( 指令功能)。它可能取出一个寄存器指示符字节,指明一个或两个寄存器操作数指示符 r A 和 r B。它还可能取出一 个四字节常数字 v a l e 。它按顺序方式计算当前指令的下一条指令的地址 va l P。也就是说, v a l P 等于 PC 的值加上巳取出指令的长 度。
译码( d ecode ) : 译码阶段从寄存器文件读入最多两个 操作数, 得到值 va l A 和/或 va lB,
通常,它 读入指令r A 和 r B 字段指明的寄存器, 不过有些指令是读寄存器r% s p 的。
执行( exec ute ) : 在执行阶段, 算术/逻辑单元( ALU ) 要么执行指令指明的操作(根据 江u n 的值),计算 内存引用的 有效地址 , 要么增加或减少栈指针。得到的值 我们称为 v a l E。在此 ,也 可能设置条件码。对一条条件传送指令来说, 这个阶段会检验条件码和传送条件(由辽u n 给出), 如果条件成立, 则更新目标寄存器。同样,
对一条跳转指令来说,这个阶段会决定是不是应该选择分支。
访存( m em o r y ) : 访存阶段可以将数据写入内存,或者从内存读出数据。读出的值
为 va l M。
写回 ( w rit e back) : 写回阶段最多可以写两个结果到寄存 器文件。
更新 PC(PC update): 将 PC 设置成下一条指令的地址。
处理器无限循环, 执行这些阶段。在我们简化的实现中 , 发生任何异常时, 处理器就会停止 : 它执行 ha lt 指令或非法指令,或 它试图读或者写非法地址。在更完整的设 计中, 处理器 会进入异常处理模式, 开始执行由异常的类型决定的特殊代码。
从前面的讲述可以看出, 执行一条指令是需要进行很多处理的。我们不仅必须执行指令所表明的操作,还必须计算地址、更新栈指针,以及确定下一条指令的地址。幸好每条 指令的整个流程 都比较相似。因为我们想使硬件数量尽可能少, 并且最终将把它映射到一个二维的集成电路芯片的表面,在设计硬件时,一个非常简单而一致的结构是非常重要 的。降低复杂 度的一种方法是让不同的指令共享尽 量多的硬件。例如, 我们的每个处 理器设计都只 含有一个算术/逻辑单元 , 根据所执行的指令类型的不同, 它的使用方式也不同。在硬件上复制逻辑块的成本比软件中有重复代码的成本大得多。而且在硬件系统中处理许 多特殊情况和特性要比用软件来处理困难得多。
我们面临的一个挑战是将每条不同 指令所需要的计算放入到上述那个通用 框架中。我们会使用图 4-1 7 中所示的代码来描述不同 Y8 6-64 指令的处理。图 4-18 ~ 图 4- 21 中的表描述了不同 Y8 6-6 4 指令在各个阶段是怎样处理的。很值得仔细研究一下这些 表。表中的这种格式很容易映射到硬件。表中的每一行都描述了一个信号或存储状态的分配(用分配操 作- 来表示)。阅读时可以把它看成是从上至下的顺序求值。当我们将这些计算映射到硬件时,会发现其实并不需要严格按照顺序来执行这些求值。
1 | OxOOO: | 30f20900000000000000 I | irmovq $9, %rdx | |
---|---|---|---|---|
2. | OxOOa: | 30f31500000000000000 I | irmovq $21, %rbx | |
3 | Ox014: | 6123 I | subq %rdx, %rbx | # subtract |
4 | Ox016: | 30f48000000000000000 I | irmovq $128,%rsp | # Problem 4.13 |
5 | Ox020: | 40436400000000000000 I | rmmovq %rsp, 100(%rbx) | # store |
6 | Ox02a: | a02f I | pushq %rdx | # push |
7 | Ox02c: | bOOf I | popq %rax | # Problem 4. 14 |
8 | Ox02e: | 734000000000000000 I | je done | # Not taken |
9 1◊ | Ox037: Ox040: | 804100000000000000 I I | call proc done: | # Problem 4. 18 |
11 | Ox040 : | 00 I | halt | |
12 | Ox041: | I | proc: | |
13 | Ox041: | 90 I | ret | # Return |
14 | I |
图4-17 Y86-64 指令序列示例。我们会跟踪这些 指令通过各个 阶段的处理
图 4-18 给出了对 OPq ( 整数和逻辑运算)、r r mo v q ( 寄存器-寄存器传送)和ir mo v q ( 立即数-寄存器传送)类型的指令所需的处理。让我们先来考虑一下整数操作。回顾图 4- 2 , 可以看到我们小心 地选择了指 令编码, 这样四个整数操作 ( a d d q、s ub q、a nd q 和 x or q ) 都有相同的 i c o d e 值。我们可以 以相同的步骤顺序来处理它们,除 了 ALU 计算必须根据if un 中编码的具体的指 令操作来设定。
图 4-18 Y86-64 指令 OPq 、r r mo vq 和 ir mov q 在顺序实现中的计算 。这些指令计算了一个值 , 并将结果存放在寄存器中。符号 i c ode : i f u n 表明指令字节的两个组成部分 , 而r A:r B 表明寄存器指示符字节的两个组成部分。符号 M1 [x ] 表示访问(读或者写)内存位置x 处的一个字节 ,而 凶 [ x ] 表示访间八个字节
整 数操 作 指令 的处 理遵 循上面列出的通用模式。在取指阶段, 我们不需要常数字, 所以 v a l P 就计 算为 P C + 2 。在译码阶段, 我们要读两个操作数。在执行阶段, 它 们 和功能指 示符 江 u n 一起 再 提供 给 ALU , 这样一来 v a l E 就成为了指令结果。这个计算是用表达式 v a l B OP v a l A 来表达的 ,这 里 O P 代表 辽u n 指定的操作。要注意两个参数的顺序一这个顺序与 Y 8 6- 6 4 ( 和 x 8 6- 6 4 ) 的习惯是一致的。例如,指 令 s u b q %r a x , %r d x 计 算的是 R [ %r d x ] - R [ %r a x ) 的值。这些指令在访存阶段什么也不做, 而 在 写 回 阶 段 , v a l E 被写入寄 存 器r B , 然后 PC 设为 v a l P , 整个指令的执行就结束了。
田日跟踪 s ub q 指 令的 执行
作为一个例 子, 让我们来看看一条 s u b q 指令的处理过程, 这条指令是图 4- 1 7 所示目标代码的第 3 行 中的 s u b q 指令。可以看到前 面 两 条 指令分别将 寄存器%r d x 和%r b x 初 始化成 9 和 21 。我们还能看到 指 令位 于地 址 Ox 0 1 4 , 由两个宇节组成,值分别为Ox 6 1 和 Ox 2 3 。 这 条 指令处理的各个阶段如下表所示, 左边列 出 了 处理一个 OP q 指令的通用的 规则(图4- 1 8 ) , 而右边列出的是对这条具体指令的计算。
阶段 | OPq rA, rB | s ubq r 毛 dx, % r b x |
---|---|---|
取指 | icode : ifun ._ M,[ PC] | icode : ifun +- M1[ Ox014] = 6: 1 |
rA:rB +- M1[ PC + l ] | rA:rB +- M1[ 0x015] = 2: 3 | |
valP 令 - PC+ 2 | valP .- Ox014+2= Ox01 6 | |
译码 | valA …… R[ rA] | valA - R[ r 毛 dx] = 9 |
valB +- R[ rB] | valB .- R[ r% bx] = 21 | |
执行 | valE +- valB OP valA Set CC | valE +- 21- 9= 1 2 ZF 仁 - 0, SF 七 0, OF+- 0 |
访存 | ||
写回 | R[ rB] +- valE | R[ 韦 r bx] - valE= 12 |
更新 PC | PC+- valP | PC +- valP= Ox016 |
这个跟踪表明我们达到 了理 想的效果, 寄存器%r bx 设成了 12 , 三个条件码都设成
了 0 , 而 PC 加 了 2。
执行 r r mo v q 指令和执行算术运算类似。不 过, 不需要取第二个寄存器操作数。我们将 ALU 的第二个输入设为 o, 先把它和第一个操作数相加, 得到 v a l E= valA, 然后再把
这个值写到寄 存器文件。对 ir mo v q 的处理与此类似, 除了 ALU 的第一个输入为常数值va l C。另外, 因为是长指令 格式, 对于 i r mo v q , 程序计 数器必须加 1 0 。所有这些指令都不改变条件码。
练习题 4. 13 填写下表的 右边 一栏 ,这 个表描述 的是 图 4-1 7 中目标 代码 第 4 行上的
ir mo v q 指令的处 理情况 :
这条指令的 执行会 怎样 改 变寄存器 和 PC 呢?
图 4-1 9 给出了内存 读写指令r mrno v q 和 mr mo v q 所需要的处理。基本流程也和前面的一样, 不过是用 ALU 来加 v a l C 和 v a l B , 得到内存操作的有效地址(偏移量与基址寄存器值之和)。在访存阶段 , 会将寄存器值 v a l A 写到内存 , 或者从内存中读出 v a l M。
阶段 取指 | r mmo vq rA, D(rB) icode: ifun +- M1[ PC] rA:rB- M1[ PC+ l ] | mrmovq D<rB), rA icode: ifun +- M1[PC] rA,rB- M1[ PC+ l ] |
---|---|---|
valC- Ms[ PC+ 2] | valC +- Ms[ PC+ 2] | |
valP.- PC+ l O | valP- PC+ l O | |
译码 | valA +- R[rA] | |
执行 | valB +- R[rB] valE 仁 - valB+valC | va/8 +— R[ rB] valE ~ valB+valC |
访存 | Ms[ valE]- valA | valE - Ms[ valE] |
写回 | ||
R[rA]+- valM | ||
更新 PC | PC 仁 valP | PC七- valP |
图 4-19 Y86-64 指令r mmovq 和 mr movq 在顺序实现中的计算 。这些指令读或者写内存
m 跟踪 rm mo v q 指令的执行
让我们 来看看图 4- 1 7 中目标代 码的第 5 行r mmo v q 指令的处理情况。 可以 看到 ,前面的指 令已将寄存 器%r s p 初始化成 了 1 28 , 而%r bx 仍然是 s ub q 指令(第3 行)算 出来的
结果 1 2 。我们还 可以 看到 ,指 令位于地 址 Ox 0 2 0 , 有 1 0 个宇节 。前两个的 值为 Ox 4 0 和
Ox43, 后 8 个是数字 Ox 0 0 0 0 0 0 0 0 0 0 0 0 0 0 6 4 ( 十进制数 1 0 0 ) 按字 节反过来得 到的数。各个阶段的处理如下:
阶段 | 通用 | 具体 | ||||||
---|---|---|---|---|---|---|---|---|
rmrnovq rA, | D( rB) | r mmov | q %r s p, | 1 | 00{ 毛r | bx | ) | |
取指 | icode: ifun - M1[ PC] rA,rB- M1 [ PC+ l ] valP - Ms[ PC+ 2] valP 噜 - PC+ l O | ico de: ifun +– M1 [ Ox020] = 4: O rA, rB - M1 [ 0xo21] = 4: 3 valC.- Ma[ Ox022] = 100 valP +– Ox020+10 = Ox02a | ||||||
译码 | valA +- R[ rA] valB - R[ rB] | valA +- R[ 号r s p ] = 128 valB - R[ 毛 r bx ] = 12 | ||||||
执行 | valE +- valB+valC | valE +- 1 2+ 100= 112 | ||||||
访存 | M式valE] - | valA | Ms[ 112].- 128 | |||||
写回 | ||||||||
更新 PC | PC 嘈- valP | PC 令- | Ox02a |
跟踪记 录表明 这条指令的 效果就是将 1 2 8 写入 内存 地址 11 2 , 并将 PC 加 1 0 。
图 4 - 2 0 给出了处理 p u s h q 和 p o p q 指令所需的步骤。它 们可以算是最难实现的 Y 8 6- 6 4 指令了, 因为它们既 涉及访问内存, 又要 增加或减少栈指针。虽然这两条指令的流程比较相似,但是它们还是有很重要的区别。
阶段 | pushq rA | popq rA | |
---|---|---|---|
取指 | ic ode: ifun - M1 [ PC] | icode: ifun +- M1[PC] | |
rA: rB - M, [ PC+ l ] | rA, rB - M1 [ PC+ l ] | ||
valP- PC+ 2 | valP 仁 - | PC+ 2 | |
译码 | valA- R[ rA] valB - R[ r% s p] | valA - valB ._ | R[ %r s p] R[ r令 s p] |
执行 | valE 仁 - valB+ ( - 8) | valE .- | va1B+ 8 |
访存 | Ms[ valE] - valA | valE - | M正valA] |
写回 | R[ %r s p] 亡 | valE | R[ 毛 r s p] - valE |
R[ rA] - valM | |||
更新 PC | PC+- valP | PC 仁 - valP |
图 4- 20 Y8 6-64 指令 pus hq 和 po pq 在顺序实现中的计算。这些指令将值压入或弹出栈
p u s h q 指令开始时很像我们 前面讲 过的指令, 但是在译码阶段,用 %r s p 作为第 二个寄存器操作数的标识符, 将栈指针赋值为 v a l B。在执行阶段,用 ALU 将栈指针减 8 。减过 8 的值就是内存写的地址, 在写回阶段还会存回到%r s p 中。将 v a l E 作为写操作的地址 , 是遵循 Y 8 6- 6 4 ( 和 x 8 6- 6 4 ) 的惯例,也 就是在写之前, p u s h q 应该先将栈指针减去 8 , 即使栈指针的更新实际上是在内存操作完成之后才进行的。
日 日 跟踪 p us hq 指令的执行
让我们 来看 看图 4- 1 7 中 目 标代码的 笫 6 行 p u s h q 指令的 处理情 况。 此时, 寄存器 %r d x 的值为 9’ 而寄 存器%r s p 的值为 1 2 8 。 我们 还可以 看到 指令是位于地 址 Ox 0 2 a ,有两个 宇节,值 分别为 Ox a O 和 Ox 2 f 。 各个阶段 的处理如 下:
阶段 | 通用 | 具体 |
---|---|---|
pushq rA | pushq r% dx | |
取指 | icode: ifu n +- M, [ PC] rA:rB- M1[ PC + l ] valP - PC+ 2 | icode: ifun ~ M1 [ Ox02a] =a: 0 rA: rB +- M1[ 0x02b] = 2: f valP 仁 - Ox02a + 2 = Ox02c |
译码 | valA +- R[ rA] valB - R[ r毛 s p] | valA .._ R[ r% dx] = 9 valB - R[ %rs p] = 128 |
执行 | valE 七 - valB+ ( - 8) | valE 仁 12a + <- 8) = 1 20 |
访存 | Ms[ valE] - valA | Ms[ 120]+- 9 |
写回 | R[ %r s p] - valE | R[ %rsp]- 120 |
更新 PC | PC 仁 - valP | PC+- Ox02c |
跟踪记录表明 这条指令的效果就是将%r s p 设 为 120 , 将 9 写入 地 址 120 , 并将 PC
加 2。
pop q 指令 的执行 与 pu s hq 的执行类似,除 了 在 译 码阶段要读两次栈指针以外。这样做看上去 很多余 ,但 是 我们会看到让 v a l A 和 v a l B 都存放栈指针的值,会 使 后 面的流程跟其他的 指令更相似,增 强 设 计 的 整体一致性。在执行阶段, 用 ALU 给栈指针加 8 , 但是用没加 过 8 的原 始值作为内存操作的地址。在写回阶段, 要用加过 8 的栈指针更新栈指 针寄存器 , 还要将寄存器 rA 更新为从内存中读出的值。用没加过 8 的值作为内存读地址, 保持了 Y86-64 ( 和 x86-64) 的惯例, popq 应该首先读内存,然 后 再 增 加 栈 指 针 。
练习题 4. 14 填写 下表的右边一栏 , 这个表描述的是图 4-17 中目标代码 第 7 行 pop q
指令 的处理情况:
这条指令的执 行会怎样改 变寄 存器和 PC 呢?
练习题 4. 15 根据图 4-20 中列 出的步骤, 指令 pus hq %r s p 会有什么样 的效果? 这与练 习题 4. 7 中确定 的 Y86-64 期望的行为 一致 吗?
练习题 4. 16 假设 po pq 在写 回阶段中的两个 寄存器写 操作按照 图 4-20 列 出的 顺序进行。po pq %r s p 执行的效果会是怎 样的? 这 与 练 习题 4. 8 中 确定的 Y86-64 期 望的行为一致吗?
图 4- 2 1 表明了三类控制转移指令的处理: 各种跳转、c a l l 和r e t 。可以看到, 我们能用同前面指令一样的整体流程来实 现这些指令。
阶段 | jXX Dest | call Dest | ret |
---|---|---|---|
取指 | icode, ifun - M1[ PC ] | ico de , ifun - M1[PC] | icode: ifun +- M1 [ PC] |
vale.- Ms[PC+1] valP +- PC+9 | valC - M8[ PC+ 1] valP 仁 - PC+ 9 valP- PC+ l | ||
译码 | valA - R[ r% s p ] valB - R[ 毛 r s p] valB +- R[ r 毛 s p] | ||
执行 | valE- valB + ( - 8) valE 仁 va1B+ 8 | ||
Cnd - Cond(CC, ifun) | |||
访存 | Ms[ valE]- valP valM - Ms[ valA] | ||
写回 | R[ 号r s p]- valE R[ 号r s p] +- valE | ||
更新 PC | PC 仁 - Cnd?valC , valP | PC 牛 - vale PC +- valM |
图 4- 21 Y86-64 指令 j XX、 c a ll 和r e t 在顺序实现中的计莽。这些指令导致控制转移
同对整数操作一样,我们能够以一种统一的方式处理所有的跳转指令,因为它们的不同只在千判断是否要选择分支的时候。除了不需要一个寄存器指示符字节以外,跳转指令在取指和译码阶段都和前面讲的其他指令类似。在执行阶段,检查条件码和跳转条件来确定是否要选择分支,产生出一个一位信号 Cnd 。在更新 PC 阶段, 检查这个标志, 如果这个标志为
1, 就将PC 设为 v a l e ( 跳转目标), 如果为 o, 就设为 v a l P( 下一条指令的地址)。我们的表示法
x?a: b类似千C 语句中的条件表达式一 当 x 非零时, 它等于a , 当 x 为零时, 等于b。
田 日 跟踪 je 指令的执行
让我们来看看图 4- 1 7 中目标 代码的笫 8 行 j e 指令的 处理情况。 s u b q 指令(第3 行) 已经将 所有的 条件码都 置为了 o, 所以 不会选择 分支。该指令位于地 址 Ox 0 2 e , 有 9 个
宇节。 第一 个字节的值 为 Ox 7 3 , 而剩下的 8 个宇 节是数宇 Ox 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 0 按宇节反过来得到的数 ,也 就是跳 转的目标 。各个阶段的 处理如 下:
就像这个跟踪记 录表明 的那样 ,这条指 令的效果就是将 PC 加 9。
心 练习题 4 . 17 从指令 编码(图4- 2 和图 4- 3 ) 我们 可以 看出,r mmo v q 指令是 一类更通用的、包 括条 件转移在内 的指 令的 无条 件版本。 请 给 出你 要如何 修 改下 面r r mo v q 指令
的步骤 , 使之也 能处 理 6 个条件 传送 指令。 看看 j XX 指令的 实现(图 4-21 ) 是如何 处理条件行为的,可能会有所帮助。
指令 c a l l 和r e t 与指令 p u s hq 和 po pq 类似, 除了我们要将程序计数器的值入栈和出栈以外。对指令 c a l l , 我们要将 va l P, 也就是 c a l l 指令后紧跟着 的那条指令的地址, 压人栈中。在 更新 PC 阶段, 将 PC 设为 v a l C, 也就是调用的目的地。对指令r e t , 在更 新 PC 阶段,我 们将 va l M, 即从栈中取出的值, 赋值给 PC。
练习题 4. 18 填写下表 的右 边一栏,这 个表描 述的是 图 4-17 中目标 代码 第 9 行 c a l l
指令的处理情况:
这条指令的执行 会怎样改 变寄 存器、 PC 和内存呢?
我们创建了一 个统一的框架, 能处理所有不同类型的 Y86-64 指令。虽然指令的行为大不相 同, 但是我们可以 将指令的处理组织成 6 个阶段。现在我们的任务是创建硬件设计来实现这些阶段,并把它们连接起来。
m 跟踪 ret 指令的执行
让我们来看看图 4-17 中目 标代码的 第 1 3 行r e t 指令的处理情况。指令的地址是
Ox041, 只有一个宇 节的 编码 , Ox 90 。 前面的 c a l l 指令将% r s p 置为 了 1 20 , 并将返回地址 Ox 040 存放在了内 存地址 1 20 中。各个阶段 的处理如 下:
通用 具体 阶段 r e t r e t | ||
---|---|---|
取指 | ico de : ifun - M1[ PC] | ico de , ifun - M1[ 0x041] = 9: 0 |
valP 仁 - PC + l | va lP 仁 - Ox041 + 1 = Ox042 | |
译码 | valA - R[ r% s p] valB +- R[ r% s p] | valA +- R[ r% s p] = 120 valB - R[ r% s p] = 120 |
执行 | val E 仁 val8 + 8 | valE +- 120+ 8 = 128 |
访存 | valM +- Ma [ valA] | valM +- Ms[ 120]= Ox040 |
写回 | R[ 毛r s p ] +- valE | R[ %r s p ] - 1 28 |
更新 PC | PC- valM | PC 仁 Ox040 |
跟踪记 录表明 这条指令的 效果就是将 P C 设 为 Ox 040 , ha l t 指令的 地址。同时 也将
%r sp 置 为 了 1 28。
4. 3. 2 SEQ 硬件结构
实现所有 Y86-64 指令所需要的计算可以被组织成 6 个基本阶段: 取指、译码、执行、访存、写回和更新PC。图 4-22 给出了一个能执行这些计算的硬件结构的抽象表示。程序计 数器放在寄存器中,在图中左下角(标明为 " PC")。然后, 信息沿着线流动(多条线组合在一起就用宽一点的灰线来表示),先向上,再 向右。同各个阶段相关的硬件单元 ( ha r dw a re uni ts) 负责执行这些处理。在右边, 反馈线路向下,包括要写到寄存器文件的更新值,以及 更新的程序计数器值。正如在 4. 3. 3 节中讨论 的那样,在 SEQ 中, 所有硬件单元的处理都 在一个时钟周期内完成。这张图省略了一些小 的组合逻辑块,还省略了所有用来操作各个硬 件单元以及将相应的值路由到这些单元的控制 逻辑。稍后会补充这些细节。我们从下往上画
处理器和流程的方法似乎有点奇。怪在开始设计流水线化的处理器时,我们会解释这么画的原因。
硬件单元与各个处理阶段相关联:
取指: 将程序计数器寄存器作为地址,指令内存读取指令的字节。PC 增加器(PC incre
程序计数器
( PC )
写回
访存
执行
取指
新PC
m enter ) 计算v a l P , 即增加了的程序计数器。译码: 寄存器文件有两个读端口 A 和 B,
从这两个端口同时读寄存器值v a l A 和 v a l B。
图 4-22 SEQ 的抽象视图,一种 顺序 实现。指令执行
过程 中的 信息 处理 沿着顺时针方向的流程进行 ,从用程序计数器 ( PC ) 取指令 开始,如图中左下角所示
执行:执 行 阶 段会根据指令的类型,将 算 术/逻辑单元( ALU ) 用于不同的目的。对整
数操作 ,它要 执 行 指 令 所 指 定 的 运算。对其他指令,它 会 作 为一个加法器来计算增加或减少栈指针 , 或者计算有效地址,或 者只是简单地加 o , 将一个输入传递到输出。
条件码寄存器CC C) 有三个条件码位。AL U 负责计算条件码的新值。当执行条件传送指令时 , 根据条件码和传送条件来计算决定是否更新目标寄存器。同样, 当执行一条跳转指令时,会 根据条件码和跳转类型来计算分支信号 Cnd 。
访存:在 执 行 访 存 操作时,数 据 内 存读出或写入一个内存字。指令和数据内存访问的是相同的 内存位置,但 是用于不同的目的。
写回 :寄 存器文件有两个写端口。端口 E 用来写 ALU 计算出来的值,而 端 口 M 用来写从数据内存中读出的值。
PC 更新: 程序计数器的新值选择自: valP, 下一条指令的地址; vale, 调用指令或
跳转指令指定的目标地址; valM, 从内存读取的返回地址。
图 4-23 更详细地给出了实现 S EQ 所需要的硬件(分析每个阶段时, 我们会看到完整的
新PC
程序计数器
(PC)更新
访存
执行
译码
i ns rt _va li
i me m_ er r or
取指
图 4-23 SEQ 的 硬件结 构 , 一 种 顺 序实现。有些控制信号以及寄存器和控制字连接没有画出来
细节)。我们看到一组和前面一样的硬件单元,但 是现在线路看得更清楚 了。这幅图以及其他的硬件图都使用的是下面的画图惯例。
白 色方 框表示时钟 寄存器。程 序计数器 PC 是 SEQ 中唯一的时钟寄存器。
浅蓝 色方框 表示硬 件单元。这 包括内存、ALU 等等。在我们所有的处理器实现中, 都会使用这一组基本的单元。我们把这些单元当作“黑盒子“,不关心它们的细节 设计。
控制逻辑块用灰色圆角矩形表示。这些块用来从一组信号源中进行选择,或者用来 计算一些布尔函数。我们会非常详细地分 析这些块, 包括给出 HCL 描述。
线路的 名字在白 色圆 圈中 说明。它们只是 线路的标识, 而不是什么硬件单元。
宽度 为字长的 数据连接用 中等粗度的线表 示。 每条这样的线实际上都代表一簇 64
根线, 并列地连在一起 , 将一个字从硬件的一个 部分传送到另一部分。
宽度为字节或更窄的数据连接用细线表示。根据线上要携带的值的类型,每条这样的线实际上都代表一簇 4 根或 8 根线。
单个位的连接用虚线来表示。这代表芯片上单元与块之间传递的控制值。
图 4-18 图 4-21 中所有的计算都有这样的性质, 每一行都代表某个值的计算(如valP), 或者激活某个硬 件单元(如内存)。图4- 24 的第二栏列出了这些计算和动作.。除了我们已经讲过的那些信号以外, 还列出了四个寄存器 ID 信号: srcA, valA 的源; srcB,
valB 的源; dstE, 写入 v a lE 的寄存器; 以及 d s t M , 写入 v a l M 的寄存器。
阶段 取指 | 计算 icode, ifun | OPq rA, rB ico de: ifun - M1[ PC] | mr mov q D( rB) , rA icode: ifun +- M1 [ PC] | |
---|---|---|---|---|
rA , rB | rA, rB - M1[ PC+ l ] | rA: rB - M, [ PC+ l] | ||
va lC . | vale - Ma[ PC+ 2] | |||
valP | valP+- PC+ Z | valP 仁 - PC + l o | ||
译码 | va lA , srcA | va/A - R[ rA] | ||
valB, srcB | valB - | R[ rB] | valB ~ R[ rB] | |
执行 | valE | valE 仁 - | valB OP valA | va lE 仁 valB + valC |
Cond. codes | Set CC | |||
访存 | Read/ write | valM +- M式valE] | ||
写回 | E port, dstE | R[ rB] - | valE | |
M port, dstM | R[ rA] - valM | |||
更 新 PC | PC | PC 仁 - valP | PC 仁 - valP |
图 4- 24 标识顺序实现中的不同计算步骤 。第二栏标识出 SE Q 阶段中正在被计算的值 , 或正在被执行的操作 。以指令 OPq 和 mr mo v q 的计算作为示例
图 中, 右边两栏给出的是指令 OP q 和 mr mo v q 的计算, 来说明要计算的值。要将这些计算映射到硬件上,我们要实现控制逻辑,它能在不同硬件单元之间传送数据,以及操作 这些单元,使得对每个不同的指令执行指定的运算。这就是控制逻辑块的目标,控制逻辑 块在图 4-23 中用灰色圆角方框表示。我们的任务就是依次经过每个 阶段, 创建这些块的详细设计。
4. 3. 3 SEQ 的时序
在介绍图 4-18 图 4-21 的表时, 我们说过要 把它们看成是用程序符号写的, 那些赋值是从上到下顺 序执行的。然而, 图 4-23 中硬件结构的操作运行根本完全不同 , 一个时
钟变化会引发一个经过组合逻辑的流,来执行整个指令。让我们来看看这些硬件怎样实现 表中列 出的这一行为。
SEQ 的实现包括组合逻辑 和两种存储器设备: 时钟寄存器(程序计 数器和条件码寄存器),随机访问存储器(寄存器文件、指令内存和数据内存)。组合逻辑不需要任何时序或控制 只要输入变化了 , 值就通过 逻辑门网络传播 。正如提到过的 那样, 我们也将读随机访问存储器看 成和组合逻辑一样的操作, 根据地址输入产生 输出字。对于较小 的存储器来说(例如寄存器文件),这是一个合理的假设,而对于较大的电路来说,可以用特殊的时钟电路来模拟这个效果。由于指令内存只用来读指令,因此我们可以将这个单元看成是组 合逻辑 。
现在还剩四个硬 件单元需要对它们的时序进行明 确的控制—— 程序计数器、条件码寄存器、数据内存和寄存器文件。这些单元通过一个时钟信号来控制,它触发将新值装载到 寄存器以及将值写到随机访问存储器。每个时钟周期,程序计数器都会装载新的指令地 址。只有 在执行 整数运算 指令时, 才会 装载条件码寄存楛。只有在执行r mrno v q 、 p u s h q 或 c a l l 指令时 , 才会写数 据内存。寄存器文件的两个写端口允许每个时钟周期更新两个程序寄存器, 不过我们可以 用特殊的寄存器 ID Ox F 作为端口地址, 来表明 在此端口不应该执行写操作。
要控制处理器中活动的时序,只需要寄存器和内存的时钟控制 。硬件获得了如图 4-18 ~ 图4-21 的表中所示 的那些赋值顺序执行一样的效果, 即使所有的 状态更新实际上同时发生, 且只在 时钟上升开始下一 个周期时。之所以能保持这样的等价性, 是由千 Y86-64 指令集的本质,因为我们遵循以下原则组织计算:
原则:从不回读
处理器从未不需要为了完成一条指令的执行而去读由该指令更新了的状态。
这条原则 对实现的成功来说至关重要。为了说明问 题, 假设我们对 p u s h q 指令的实现是先将 %r s p 减 8 , 再将更新后的%r s p 值作为写操作的地址。这种方法同前面所说的那个原则相违背 。为了执行内 存操作, 它需要先从寄存器文件中读 更新过的栈指针。然而, 我们的实现(图 4- 20 ) 产生出减后的栈指针值, 作为信号 v a l E , 然后再用这个信号既作为寄存器写的数据,也作为内存写的地址。因此,在时钟上升开始下一个周期时,处理器就可 以同时执行寄存器写和内存写了。
再举个例子来说明这条原则,我们可以看到有些指令(整数运算)会设置条件码,有些指令(跳转指令)会读取条件码,但没有指令必须既设置又读取条件码。虽然要到时钟上升 开始下一个周期时 , 才会设置条件码, 但是在任何指令试图读之前 ,它 们都会更新。
以下是汇编代码,左 边列出的是指令地址, 图 4- 25 给出了 SEQ 硬件如何处理其中第
3 和第 4 行指令:
OxOOO: | irmovq $0x100,%rbx | # %rbx <– | Ox100 |
---|---|---|---|
OxOOa: | irmovq $0x200,%rdx | # %rdx <– | Ox200 |
Ox014 : Ox016: | addq %rdx,%rbx je dest | # ir 儿 bx <– # Not taken | Ox300 CC<– 000 |
Ox01f: rmmovq %rbx, 0 (r儿Ox029: dest: halt
dx )
# M[Ox200] <– Ox300
标号为 1 ~ 4 的各个图给出了 4 个状态单元, 还有组合逻辑 , 以及状态单元之间的连接。组合逻辑被条 件码寄存器环绕着, 因为有的 组合逻辑(例如 ALU ) 产生输入到条件码寄存器, 而其他部分(例如分支计算和 PC 选择逻辑)又将条件码寄存器作为输入。图中寄
存器文件和数据内存有独立的读连接和写连接,因 为读操作沿着这些单元传播,就 好 像它们 是组合逻辑, 而写操作是由时钟控制的。
时钟
周期1 周期2: 周期3
OxO OO : irmovq $s x100 , 毛 r bx # % r b x <- - Ox l OO OxOOa: irmovq $0x200, 号 r bx # 沦r dx <- - Ox200
Ox0 1 4 addq %r d x , 马r b x # 号r b x <- - Ox 300 CC <- - 000
周期4:• .;·,,··,,O•~,,,. -x’·0 1,6.
,飞e .. -仓-.-s- f– 心丈-又二’, 令.–,玉t,·,• 七, 己-,-.1,C· 鲁 .N- o t心. t -、ak -e文., ·,.,.心, .-,歹石心`、,,乓飞L ·节· 咚<兮.分- 五-、’: $
周期5:
Ox Olf : r mmo v q % r bx , 0 (% r d x ) # M [ Ox20 0 ] <- - Ox 3 0 0
心周期3开始时
@周期4开始时
@ 周期3结束时
r毛 bx
`Ox300
图 4-25 跟踪 SEQ 的两个 执行周期。每个周期开始时, 状态 单元(程序计数器、条件 码寄存器、寄存 器文件以 及数据内 存)是根据前一条指令设置的。信号传播通过组合逻辑 , 创建出新的状态单元的 值。在下一个周期开始时 , 这些值会被加 载到状态单元中
图 4-25 中的不同颜色的代码表明电路信号是如何与正在被执行的不同指令相联系的。我们假设处理是从设置条件码开始的,按 照 ZF 、S F 和 OF 的顺序, 设 为 100。在时钟周 期 3 开始的时候(点1) ’ 状态单元保持的是第二条 i r mov q 指 令( 表中第 2 行)更新过的状态,该 指 令 用 浅灰色表示。组合逻辑用白色表示, 表明它还没有来得及对变化了的状态做出 反应。时钟周期开始时,地 址 Ox 01 4 载入程序计数器中。这样就会取出和处理 a dd q 指 令(表中第 3 行)。值沿着组合逻辑流动, 包括读随机访问存储器。在这个周期末尾(点2) , 组合逻辑为条件码产生了新的值( 000) , 程序寄存器%r b x 的 更 新 值 ,以 及程序计数器的新
值( Ox01 6) 。 在此时 , 组合逻辑已经根据 a d d q 指令被更新了,但 是状态还是保持着第二
条 ir mo v q 指令(用浅灰色表示)设置的值。
当时钟上升开始周 期 4 时(点3) , 会更新程序计数器、寄存骈文件和条件码寄存器, 因此我们用蓝 色来表示, 但是组合逻辑还 没有对这些变化做出反应, 所以用白色表示。在这个周期内, 会取出并 执行 j e 指令(表中第 4 行), 在图中用深灰色表示。因为条件码 ZF
为 o, 所以不会选择分支。在这个周期末尾(点4 ) , 程序计数器巳经产生了新值 Ox Olf 。
组合逻 辑已经根据 j e 指令(用深灰色表示)被更新过了, 但是直到下个周期开始之前, 状态还是 保持着 a d d q 指令(用蓝色表示)设置的值。
如此例所示, 用时钟来控制状态单元的更新 , 以及值通过 组合逻辑 来传播, 足够控制我们 SEQ 实现中每条指令 执行的计算了。每次时钟由低变高时, 处理器开始执行一条新指令。
3. 4 S EQ 阶段的实现
本节会设 计实现 SEQ 所需要的控制逻辑块的 HCL 描述。完整的 SEQ 的 HCL 描述请参见网络 旁注 ARC H : HCL。在此, 我们给出一 些例子, 而其他的作为练习题。建议你做做这些练习来 检验你的理 解, 即这些块是如何 与不同指令的计算需求相联系的。
我们没有讲的 那部分 SEQ 的 HCL 描述, 是不同整数 和布尔信号的定义, 它们可以作为 HCL 操作的参数。其中包括不同硬件信号的名字, 以及不同指令代码、功能码、寄存器名字、 ALU 操作和状态码的常数值。只列出了那些在控制逻辑中必须被显式引用的常数。图 4- 26 列出了我们使用的 常数。按照习惯, 常数值都是大写的。
图 4- 26 HCL 描述中使用的常数值。这些值表示的是指令 、功能码、寄存器 ID、AL U 操作和状态码的编码
除了图 4-18 图 4- 21 中所示的指令以外,还 包括了对 n a p 和 h a 吐 指令的处理。n a p
指令只是简单地经过各个阶段,除 了 要 将 PC 加 1 , 不 进 行 任 何 处 理 。 ha lt 指 令 使 得 处 理器状态被设 置为 HLT , 导致处理器停止运行。
取指阶段
如图 4-27 所示 , 取 指 阶 段 包 括 指 令 内 存 硬 件 单 元 。 以 PC 作 为 第 一 个 字 节(字 节 0 ) 的地址 , 这 个 单 元一 次从 内 存 读 出 10 个 字 icode ifun rA rB valC valP
节。第一个字节被解释成指令字节,(标
号为 “Split” 的单 元)分为 两个 4 位 的数 。然 后 , 标 号 为 " icode " 和 " if un " 的控制逻辑块计算指令和功能码,或者 使之等千从内存读出的值, 或 者 当 指 令地 址 不 合 法 时( 由 信 号 i me m_ er r o r 指明 ), 使 这 些 值 对 应 于 n op 指 令 。 根 据i c od e 的 值 , 我 们 可以计算 三个一 位 的信号(用虚线表示):
instr_valid: 这个字节对应千一个合法的 Y86-64 指令吗? 这个信号 用来发现不合法的指令。
need_regids: 这个指令包括一个寄存器指示符字节吗?
need_valC: 这个指令包括一个常数字吗?
图 4-27 SEQ 的取指阶段。以PC 作为起始地址,从指令内 存中读出 10 个字节。根据 这些字节, 我们产生出各个 指令字段。PC 增加模块计算信号valP
(当指令地 址越界时 会产 生的)信号 i ns tr _v a l i d 和 i me m_ er r or 在 访 存 阶 段 被 用 来产 生 状 态 码 。
让 我 们 再 来 看一 个 例子 , n e e d _r e g i d s 的 H CL 描述只是确定了 i c od e 的值是否为一
条带有寄存器指示值字节的指令。
bool need_regids =
icode in { IRRMOVQ, IOPQ, IPUSHQ, IPOPQ, IIRMOVQ, IRMMOVQ, IMRMOVQ };
练 习题 4. 19 写 出 SEQ 实现 中信 号 ne e d _v a l C 的 HCL 代码。
如图 4-27 所示 , 从 指 令 内 存 中 读 出 的 剩 下 9 个 字 节 是 寄 存 器 指 示 符 字 节 和 常 数 字的 组 合 编 码 。 标 号 为 " A lig n" 的 硬 件 单 元 会 处 理 这些 字 节 , 将 它 们 放 入 寄 存 器 字 段 和常 数 字 中 。 当 被 计 算 出 的 信 号 ne e d _r e g i d s 为 1 时 , 字 节 l 被 分 开 装 入 寄 存 器 指 示符 r A 和 rB 中 。否 则 , 这 两 个 字 段 会 被 设 为 Ox F( RNONE) , 表 明 这 条 指 令 没 有 指 明 寄 存 器。回 想 一 下(图 4-2 ) , 任何只有一个寄存器操作数的指令,寄存器指示值字节的另一个字 段都设为 Ox F( RNONE) 。 因 此 , 可 以 将 信 号 r A 和 rB 看 成 , 要 么 放 着 我 们 想 要 访 问 的寄存 器 , 要 么 表 明 不 需 要 访 问 任 何 寄 存 器 。 这 个 标 号 为 " A lig n" 的 单 元 还 产 生 常 数字 v a l C。 根 据 信 号 ne e d _r e g i d s 的 值 , 要 么 根 据 字 节 1 ~ 8 来 产 生 v a l e , 要 么 根 据 字 节 2~ 9 来 产 生 。
PC 增加器硬件单元根据当前的 PC 以 及 两 个 信 号 ne e d _r e g i d s 和 ne e d _ v a l C 的 值,
产 生 信 号 v a l P。对 于 PC 值 p 、ne e d _r e g i d s 值r 以 及 ne e d _ v a l C 值 i’ 增 加 器 产 生值
p + l + r + 8 i 。
2 译码和写回 阶段
图 4-28 给出了 SEQ 中实现译码和写回阶段的逻辑的详细情况。把这两个 阶段联系在一起是因为它们都要访问寄存器文件。
寄存器文件有 四个端口。它支持同时进行两个读(在端口 A 和 B 上)和两个写(在端口
E 和 M 上)。每个端口都有一个地址连接和一个数据 Cnd valA valB valM valE
连接, 地址连接是一个寄存器 ID , 而数据连接是一组 64 根线路 ,既 可以作为寄存 器文件的输出字(对读端口来说),也可以作为它的输入字(对写端口来说)。两个读端口的 地址输入 为 s r c A 和 sr c B, 而两个写端口的地 址输入为 d s t E 和 d s 七M。 如果某个地址端口上的值为特 殊标识符 Ox F ( RNONE) , 则表明不需要访问寄存器。
根据指令代码 i c o de 以及寄存器指示值r A 和 icode rB, 可能还会 根据执行阶段计算出的 Cn d 条件信号, 图 4- 28 图 4-28 底部的四个块 产生出四个不同的寄存器文件的
寄存器 ID。寄存器 ID sr c A 表明应该读哪个寄存器以产生 va l A。所需 要的值依赖于指令类型, 如图 4-18 ~ 图 4-21 中译码阶段第一行中所示。将所有这些条目都整合到 一个计算中就得到下面的 sr c A 的 HCL 描述
(回想 RRSP 是%r s p 的 寄存器 ID) :
word srcA = [
rA rB
SEQ 的 译 码 和写 回 阶段 。指令字段译码,产生寄存器文件使用 的四个地址(两个读和两个写)的 寄存器标识符。从寄存器文件 中读出的 值 成 为 信 号 va l A 和va l B。 两 个 写 回 值 val E 和va l M 作 为写 操作的数据
icode in { IRRMOVQ, IRMMOVQ, IOPQ, IPUSHQ } : rA; icode in { IPOPQ, IRET} : RRSP;
1 : RNONE; # Don’t need register
];
区 综习题 4. 20 寄存器 信号 sr c B 表明 应该 读 哪个寄 存器以 产 生信 号 v a l B。所需 要的值如图 4-18 ~ 图 4-21 中译 码 阶段 第二 步所 示。 写 出 s r c B 的 HCL 代码。
寄存器 ID d s t E 表明 写端口 E 的目的寄存器,计 算出来的值 v a l E 将放在那里。图 4-18~ 图 4-21写回阶段第一步表明了这一点。如果我们暂 时忽略条件移 动指令, 综合所有不同指令的 目的寄存器, 就得到下 面的 d s t E 的 HCL 描述:
# WARNING: Conditional move not implemented correctly here
word dstE = [
icode in { IRRMOVQ} : rB; icode in { IIRMOVQ, IDPQ} : rB;
icode in { IPUSHQ, IPOPQ, ICALL, IRET} : RRSP;
1 : RNONE; # Don’t write any register
];
我们查看执行 阶段时, 会重新审视这个信号 , 看看如何实现条件传送。
" 练习 题 4. 21 寄存器 ID d s t M 表 明 写 端 口 M 的 目 的寄存器, 从 内存 中读 出 来的值v a l M 将放 在那里 , 如图 4-18 图 4-21 中写 回 阶 段 第 二 步所 示。 写 出 d s t M 的 HCL 代码。
区 练习题 4. 22 只有 p o p q 指令会同 时用 到寄 存器 文 件的 两个 写 端 口。 对于指令 p o p q
%rsp, E 和 M 两个写 端口会用 到 同 一 个地 址, 但是写 入的数据不 同。 为 了 解决这个冲 突, 必须对两个写 端口设 立 一个 优先级, 这样一来, 当同 一个周期内两个写 端口都试图对一个寄存器进行写时,只有较高优先级端口上的写才会发生。那么要实现练习题 4. 8 中确定的行为 , 哪个端口该 具有较高 的优先级呢?
3 执行阶段
执行阶段包括算术/逻辑单元 ( ALU ) 。这个单元根据 a l u f u n 信号的设置,对 输 入 a l uA 和 a l u B 执行 ADD、SUBT RACT 、AND 或 EXCLUSIVE
OR 运算。如图 4-29 所示, 这些数据和控制信号是由三个 控制块产生的。ALU 的输出就是 v a l E 信号。
在图 4-18 图 4- 21 中 , 执行阶段的第一步就是每条指令的 ALU 计算。列 出的操作数 a l u B 在
Cnd
+
icode ifun
valE
valC valA valB
前面,后 面是 a l uA, 这样是为了保证 s u b q 指令 图 4-29
是 v a l B 减去 v a l A。可以看到, 根据指令的类型, a l u A 的值可以是 v a l A、v a l e , 或者是—8 或 十8。因 此 我们可以用下面的方式来表达产生 a l u A 的控制块的行为:
word aluA = [
icode in { IRRMOVQ, IOPQ} : valA;
S EQ 执行阶段。 ALU 要么为整数
运算指令执行操作,要么作为加法器。根据 ALU 的值, 设置条件码寄存器。检测条件码的 值, 判断是否该选择分支
icode in { IIRMOVQ, IRMMOVQ, IMRMOVQ} : valC; icode in { ICALL, IPUSHQ} : -8;
icode in { IRET, IPOPQ} : 8;
# Other instructions don’t need ALU
];
练习题 4. 23 根据图 4-18 图 4- 21 中执行阶段第 一 步的 第 一个 操作数, 写 出 SEQ 中
信号 a l u B 的 HCL 描述。
观察 ALU 在执行阶段执行的操作, 可以看到它通常作 为加法器来使用。不过, 对于
OPq 指令 , 我们 希望 它使 用 指 令 i f u n 字段 中 编码的操作。因此, 可以将 ALU 控制的
HCL 描述写成:
word alufun = [
icode == IDPQ : ifun;
1 : ALUADD;
];
执 行 阶 段 还包括条件码寄存器。每次运行时, ALU 都会产生三个与条件码相关的信号 零 、符 号 和 溢出。不过, 我们只希望在执行 OPq 指令时才设置条件码。因此产生了一 个信号 s e 七_ c c 来 控制是否该更新条件码寄存器:
bool set_cc = icode in { IDPQ };
标 号 为 " co n d " 的硬件单元会根据条件码和功能码来确定是否进行条件分支或者条件数 据传送(图4-3) 。它产生信号 Cn d , 用于设置条件传送的 d s t E , 也用在条件分支的下一个 PC 逻辑中。对于其他指令,取 决 于 指 令 的 功 能 码 和 条 件 码的设置, Cn d 信号可以被设置 为 1 或者 0。但是控制逻辑会忽略它。我们省略这个单元的详细设计。
练习题 4. 24 条件传送指令(简称 c mo v XX) 的指令代 码 为 I RRMOVQ。 如图 4- 28 所 示, 我们 可以用 执行 阶 段 中产 生 的 Cn d 信 号 实现这
些指 令。修 改 d s t E 的 HCL 代 码 以 实 现 这 些
指令。
- 访存阶段
访存阶段的任务就是读或者写程序数据。如 图 4-30 所示,两 个 控制块产生内存地址和内存输入数据(为写操作)的值。另外两个块产生表明应该执行读操作 还是写操作的控制信号。当执行读操作时, 数据内 存产生值 v a l M。
图 4-18 ~ 图 4- 21 的 访 存 阶 段给出了每个指令类型所需要的内存操作。可以看到内存读和写的 地 址 总 是 v a l E 或 v a l A。 这 个 块 用 HCL 描 述就是:
word mem_addr = [
instr_valid imem_error
图 4-30
icode
SEQ 访存阶段。数据内存既可以写,也可以读内存的值。从内存中读出的值就形成了信号 val M
icode in { IRMMOVQ, IPUSHQ, ICALL, IMRMOVQ} : va l E;
icode in { IPOPQ, IRET} : valA;
# Other instructions don’t need address
];
练习题 4. 25 观察图 4-1 8 ~ 图 4-21 所 示 的 不 同 指令的 访存操 作, 我 们 可 以看到 内存写的 数据 总是 v a l A 或 v a l P。写 出 S E Q 中信号 me m_ d a 七a 的 H CL 代码。
我们 希望只为从内存读数据的指令设置控制信号 me m_r e a d , 用 HCL 代码表示就是:
bool mem_read = icode in { IMRMOVQ, IPOPQ,
IRET };
练习题 4. 26 我 们 希 望 只 为 向 内存 写 数 据的 指令设 置 控 制 信 号 me m wr i t e 。 写出
S E Q 中信号 me m_ wr i t e 的 HCL 代码。
访存 阶段最后的功能是根据取值阶段产生的 i c od e 、i me m_ er r or 、 i n s tr _ v a l i d 值以及数据内存产生的 dme m_ er r or 信 号 ,从 指 令 执行的结果来计算状态码 S t a t 。
练习题 4. 27 写出 St a t 的 HCL代码,产 生四 个
状态码 SAOK、SADR、SINS 和S HLT( 参见图 4-26)。
更新 PC 阶段
S E Q 中最后一个阶段会产生程序计数器的新值
(见图4-31) 。如图 4-18 ~ 图 4-21 中 最后 步骤所示, 依据指令的类型和是否要选择分支, 新的 PC 可能
是 v a l C、v a l M 或 v a l P。用 HCL 来描述这个选择就是:
“Word neY_pc = [
# Call. Use instruction constant icode == ICALL: valC;
图 4-31
SEQ 更新 PC 阶段。根据指令代码
和分支标志, 从信号 val e、val M
和 val P 中选出下一个PC 的值
# Taken br an ch . Use instruction constant icode == IJXX && Cnd: valC;
# Completion of RET instruction. Usevalue from stack
icode == IRET: valM;
# Default: Use incremented PC
1 : valP;
];
6. SEQ 小结
现在我们已经 浏览了 Y86-64 处理器的一个完整的设计。可以 看到, 通过将执行每条不同指令所需的步骤组织成一个统一的流程,就可以用很少量的各种硬件单元以及一个时钟来控制计算的顺序,从而实现整个处理器。不过这样一来,控制逻辑就必须要在这些单元之间路由信号, 并根据指令类型和分支条件产 生适当的控制信号。
SEQ 唯一的问题就是它太慢 了。时钟必须非常慢, 以使信号能在一个周期内传播所有的阶段。让我们来看看处 理一条r e t 指令的例子。在时钟周期起始时, 从更新过的 PC 开始, 要从指令内存中读出指令, 从寄存器文件中 读出栈指针, ALU 将栈指针加 8 , 为了得到程序计 数器的下一个值,还要 从内存中读出返回地址 。所有这一切都必须在这个周期结束之前完成。
这种实现方法不能充分利用硬件单元,因为每个单元只在整个时钟周期的一部分时间内才被使用。我们会看到引入 流水线能获得更好的性能。
4. 4 流水线的通用原理
在试图设计一个流水线 化的 Y86-64 处理器之前, 让我们先来看看 流水线化的系统的一些通用属性和原理。对于曾经在自助餐厅的服务线上工作过或者开车通过自动汽车清洗线的人,都会非常熟悉这种系统。在流水线化的系统中,待执行的任务被划分成了若干个独立的阶段。在自助餐厅,这些阶段包括提供沙拉、主菜、甜点以及饮料。在汽车清洗 中,这些阶段包括喷水和打肥皂、擦洗、上蜡和烘干。通常都会允许多个顾客同时经过系统,而不是要等到一个用户完成了所有从头至尾的过程才让下一个开始。在一个典型的自助餐厅流水线上, 顾客按照相同的顺 序经过各个 阶段, 即使他们并 不需要某些菜。在汽车清洗的情况中,当前面一辆汽车从喷水阶段进入擦洗阶段时,下一辆就可以进入喷水阶段了。通常,汽车必须以相同的速度通过这个系统,避免撞车。
流水线化 的一个重要特性 就是提高了系统的吞吐量( t h ro ug h p u t ) , 也就是单位时间内
服务的顾客总数, 不过它也会轻微地增 加延迟Cla t e ncy ) , 也就是服务一个用户所需要的时间。例如,自助餐厅里的一个只需要甜点的顾客,能很快通过一个非流水线化的系统,只 在甜点阶段停留。但是在流水线 化的系统中, 这个顾客如果试图直接去甜点阶段就有可能招致其他顾客的愤怒了。
4. 4. 1 计算流水线
让我们把注意力放到计算流水线上来,这里的"顾客”就是指令,每个阶段完成指令 执行的一部分。图 4-32a 给出了一个很 简单的非流水线化的硬件系统例子。它是由一些执行计算的逻辑以 及一个保存计算结果的寄存器组成的。时钟信号控制在每个特定 的时间间隔加载寄存器 。CD 播放器中的译 码器就是这样的一个系统。输入信号是从 CD 表面读出的 位, 逻辑电 路对这些位进行译码, 产生音频信号。图中的计算块是用组合逻辑来实现的,意味着信号会穿过一系列逻辑门,在一定时间的延迟之后,输出就成为了输入的某个 函数。
300 ps 20 ps
延迟=320 ps
吞吐量=3.12 GIPS
时钟
a ) 硬件:未 流水线化的
I1 I2 I3
b ) 流水线图
图 4-32 非 流水线化的计算 硬件。每个 320ps 的 周 期 内 , 系 统 用
300ps计算 组 合 逻辑 函数, 20ps 将结果 存到输出寄存器中
在现代 逻辑设计中, 电 路 延迟以微微秒或皮秒( picosecond , 简写成 " ps” ) , 也就是
10-1 2 秒为单位来计算。在这个例子中, 我们假设组合逻辑需要 300 ps , 而加载寄存器需要
20ps。图 4-32 还给出了一种时序图 ,称 为流水线图 ( pipeline diag ra m ) 。在图中, 时 间 从左向右流动 。从上到下写着一组操作(在此称为 11、 12 和 13 ) 。实 心 的 长 方形表示这些指令执行的时间。这个实现中,在开始下一条指令之前必须完成前一个。因此,这些方框在 垂直方向上 并没有相互重叠。下面这个公式给出了运行这个系统的最大吞吐量:
1 条指令 l OOO ps
吞吐量=(20+300)ps l n8s
:::::::: 3. 12 GIPS
我们以 每秒千兆条指令 CGIPS ) , 也 就 是 每秒十亿条指令, 为单位来描述吞吐量。从头到尾执行一条指令所需要的时间称为延迟( late ncy) 。在此系统中,延 迟为 320 ps , 也就是吞吐量的倒数。
假设将系统执行的计算分成三个阶段CA、B 和 C)’ 每个阶段需要 l OOps , 如图 4-33 所
100 ps 20 ps 100 ps 20 ps 100 ps 20 ps
延迟= 360 ps
吞吐噩= 8.33 GIPS
时钟
a ) 硬件: 三阶段流水线
11 公·、r :· B - ·. C '
12 · • B ··c.:
13 A· · t fa= 1’ 气
时间
b ) 流水线图
图 4-33 三阶段流水线化的计算硬件。计算被划分为三个阶段 A、B 和 C。每经过一个 120ps的 周期 , 每条指令就行进通过一个阶段
8 l ns = l0- 9 s 。
示。然后在各个阶段之间放上流水线寄存器 ( pipeline register), 这样每条指令都会按照三步经过这个系统,从 头 到 尾需要三个完整的时钟周期。如图 4-33 中的 流水线图所示, 只要 Il 从 A 进入 B , 就可以让 12 进入阶段 A 了,依 此类推。在稳定状态下, 三个阶段都应该是活动的,每个时钟周期,一条指令离开系统,一条新的进入。从流水线图中第三个时钟周期就能看出这一点, 此时, Il 是 在 阶段 C , 12 在阶段 B, 而 13 是在阶段 A。在这个系统 中 , 我们将时钟周期设为 l 00+ 20 = 120 ps , 得到的吞吐量大约为 8. 33 GIPS 。因为处理一条指令需 要 3 个时钟周期 ,所 以 这条流水线的延迟就是 3 X 120 = 360ps 。我们将系统吞吐最提高到原来的 8. 33/ 3. 12 = 2. 67 倍 , 代价是增加了一些硬件,以 及 延 迟的少量增加( 360 / 320 = 1. 12) 。延迟变大是由千增加的流水线寄存器的时间开销。
4. 4. 2 流水线操作的详细说明
为了更好地理解流水线是怎样工作的,让我们来详细看看流水线计算的时序和操作。图 4-34 给出了前面我们看到过的三阶段流水线(图4-33) 时钟
的流水线图。就像流水线图上方指明的那样,流水线阶 Ii
’ ,
;,’
段之间的指令转移是由时钟信号来控制的。每隔 120 ps ,
信号从 0 上升至 1’ 开始下一组流水线阶段的计算。
图 4-35 跟踪 了 时 刻 240 360 之间 的电 路 活 动 , 指 令 I1 经 过 阶段 C, 12 经 过阶段 B, 而 13 经 过 阶段
I2 A B C
I3 A B . C
0 120 240 360 480 600
时间
A。就在时刻 240 (点 1 ) 时钟上升之前 , 阶 段 A 中计算 图 4-34 三 阶段流水线的时序 。时钟的 指 令 12 的 值 已 经到达第一个流水 线寄存 器的输入, 信号的上升沿控制指令从一
但 是 该 寄存器的状态和输出还保持为指令 Il 在阶段 A
中计算的值。指令 11 在 阶 段 B 中计算 的值 巳经到达第
个流水线阶段移动到下一个
阶段
二个流水线寄存器的输入。当时钟上升时,这些输入被加载到流水线寄存器中,成为寄 存器的输出(点2 ) 。另外 , 阶 段 A 的输入被设置成发起指令 I3 的计算。然后信号传播通过各个阶段的组合逻辑(点3 ) 。就像图中点 3 处的曲线化的波阵面( cur ved wavefront) 表明的那样, 信 号 可能以不同的速率通过各个不同的部分。在时刻 360 之前,结 果 值到达流水线寄存器的输入(点4) 。 当 时 刻 360 时钟上升时, 各 条 指 令 会 前 进 经 过一个流水线阶段。
从这个对流水线操作详细的描述中, 我们可以看到减缓时钟不会影响流水线的行为。信号传播到流水线寄存器的输入, 但是直到时钟上升时才会改变寄存器的状态。另一方面,如 果 时 钟 运行得太快,就 会 有灾 难 性 的 后果 。值 可能会来不及通过组合逻辑,因 此当时钟上升时,寄存器的输入还不是合法的值。
根据对 SEQ 处理器时序的讨论( 4. 3. 3 节), 我们看到这种在组合逻辑块之间采用时钟寄存器的简单机制, 足够控制流水线中的指令流。随着时钟周而复始地上升和下降, 不同的 指 令 就会 通过流水线的各个阶段, 不会相互干扰。
4. 4. 3 流水线的局限性
图 4-33 的 例子给出了一个理想的流水线化的系统,在 这个系统中, 我们可以将计算分 成 三个相互独立的阶段,每个 阶段需要的时间是原来逻辑需要时间的三分之一。不幸的是,会出现其他一些因素,降低流水线的效率。
时钟 二
I1 I2 13
时间120 庈冰 t /360 G) ®@@
© 时间= 239
100 ps 20 ps 100 ps 20 ps 100 ps 20 ps
® 时间= 241 时钟
100 ps 20 ps 100 ps 20 ps 100 ps 20 ps
@时间= 300 时钟
时钟
@ 时间= 359
100 ps
时钟
图 4-35 流水线操作的一个时钟周期。在时刻 240( 点 1) 时钟上升之前,指 令 11 和 12 已 经 完 成 了 阶 段B 和 A。在时钟上升后,这 些 指 令开始传送到阶段 C 和 B, 而指令 13 开始经过阶段 A C 点 2 和 3 ) 。就在时钟开始再次上升之前, 这些指令的结果就会传到流水线寄存器的输人(点4 )
不一致的划分
图 4- 36 展示的系统中和前面一 样, 我们将计算划分为了三个阶段, 但是通过这些阶 段的延迟从 50 ps 到 1 50 ps 不等。通过所有阶段的延迟和仍然为 300 ps 。不过,运 行时钟的速率是由 最慢的阶段的延迟限制的。流水线图表明, 每个时钟周期, 阶段 A 都会空闲(用白色方框表示) l OOps , 而阶段 C 会空闲 50 ps 。只有阶段 B 会一直处于活动状态。我们必须将时钟周期设 为 1 50 + 20 = l 70ps, 得到吞吐量为 5. 88 GIPS 。另外, 由于时钟周期减慢
了,延 迟也增加到了 510ps 。
50 ps 20 ps 150 ps 20 ps 100 ps 20 ps
延迟= 510 ps
吞吐量= 5.88 GIPS
I1 区l
I2
时钟
a ) 硬件: 三阶段流水线, 不一致的阶段延迟
I3
时间
图 4-36
b ) 流水线图
由不一致的阶段延迟造成的流水线技术的局限性。系统的吞吐量受最慢阶段的速度所限制
对硬件设计者来说,将系统计算设计划分成一组具有相同延迟的阶段是一个严峻的挑战。通常,处理 器中的某些硬件单元,如 ALU 和内存,是 不 能 被划分成多个延迟较小的单元的。这就使得创建一组 平衡的阶段非常困难。在设计流水线化的 Y86-64处理器中, 我们不会过于关注这一层次的细节,但是理解时序优化在实际系统设计中的重要性还是非常重要的。
练习题 4. 28 假设我 们 分析图 4-32 中 的 组合逻辑, 认为 它 可以分成 6 个块, 依次命名 为 A F , 延迟分别为 80 、 30、60 、50 、70 和 l Ops , 如下图所示:
80 ps 30 ps 60 ps 50 ps 70 ps
在这些块之间插入流水线寄存器,就得到这一设计的流水线化的版本。根据在哪里插入流水线寄存器,会出现不同的流水线深度(有多少个阶段)和最大吞吐量的组 合。假设每个流水线寄 存器的延迟 为 20 ps。
只插入一个寄存器,得到一个两阶段的流水线。要使吞吐量最大化,该在哪里插入寄存器呢?吞吐量和延迟是多少?
- 要使一个三阶段的流水线的吞吐量最大化,该将两个寄存器插在哪里呢?吞吐量和延迟是多少?
- 要使一个四阶段的流水线的吞吐量最大化,该将三个寄存器插在哪里呢?吞吐量和延迟是多少?
- 要得到一个吞吐量最大的设计,至少要有几个阶段?描述这个设计及其吞吐量和延迟。
流水线过深,收益反而下降
图 4-37 说明了流水线技术的另一个局限性。在这个例子中, 我们把计算分成了 6 个阶 段 , 每个 阶段需要 50ps。在每对阶段之间插入流水线寄存器就得到了一个六阶段流水线 。 这 个 系统的最小时钟周期为 50 + 20 = 70ps , 吞吐量为 14. 29 G IPS 。因此, 通过将流
水线的阶 段数加倍 , 我们将性能提高了 14. 29/8. 33=1. 71。虽 然我们将每个计算 时钟的时间缩短了两倍,但是由于通过流水线寄存器的延迟,吞吐量并没有加倍。这个延迟成了流 水线吞吐量的一个制约因素。在我们的新设计中,这个延迟占到了整个时钟周期的 28. 6 %。
50 ps 20 p
t
时钟 延迟= 420 ps, 吞吐量= 14.29 GIPS
图 4-37 由开销造成的流水线技术的局限性。在组合逻辑被分成较小的块时,
由寄存器更新引起的延迟就成为了一个限制因素
为了提高时钟频率,现代处理器采用了很深的(1 5 或更多的阶段)流水线。处理器架构师将指令 的执行划分成很多非常简单的 步骤, 这样一来每个阶段的延 迟就很小。电路设计者小心地设计流水线寄存器,使其延迟尽可能得小。芯片设计者也必须小心地设计时钟传播网 络,以保证时钟在整个芯片上同时改变。所有这些都是设计高速微处理器面临的挑战。
练习题 4. 29 让我们来 看看 图 4-32 中的 系统 , 假设将 它划分 成 任意 数 量 的流水线 阶段 k , 每个阶段有 相同的 延迟 300 / k , 每个 流水 线寄 存器的延迟 为 20 ps。
- 系统的 延迟和吞 吐量写 成 k 的函数是 什 么?
- 吞吐量的上限等千多少?
4. 4. 4 带反馈的流水线系统
到目前为止,我们只考虑一种系统,其中传过流水线的对象,无论是汽车、人或者指 令, 相互都是 完全独 立的。但 是, 对于像 x86-64 或 Y86-64 这样执行机器程序的系统来说,相 邻指令之间很 可能是相关的。例如, 考虑下面这个 Y86-64 指令序列:
在这个包含 三条 指令的序列中, 每对相邻的指令之间都有数据相关 ( dat a dependen
cy)’ 用带圈的寄存器名字和它们之间的箭头来表示。ir mo v q 指令(第1 行)将它的结果存放在%r a x 中, 然后 a d d q 指令(第2 行)要读这个值 ; 而 a d d q 指令将它的结果存放在%r b x 中,mr mo v q 指令(第3 行)要读这个值。
另一种相关 是由于指令控制流造成的顺序相关。来看看下面这个 Y86-64 指令序列:
loop:
subq %rdx,%rbx
jne targ
irmovq $10,%rdx jmp loop
targ:
halt
j ne 指令(第3 行)产生了一个控制相 关( cont ro l dependency) , 因为条件测试的结果会决定要执行的新指令是 ir movq 指 令(第 4 行)还是 ha 止 指 令(第 7 行)。在我们的 SEQ 设计中, 这些相关都是由反馈路径来解决的, 如 图 4- 22 的右边所示。这些反馈将更新了的寄存器值向下传送到寄存器文件, 将新的 PC 值向下传送到 PC 寄存器。
图 4-38 举例说明了将流水线引入含有反馈路径的系统中的危险。在原来的系统(图4-38a) 中, 每条指令的结果都反馈给下一条指令。流水线图(图4-386)就说明了这个情况, 11 的结果成为 12 的输入,依 此类推。如果试图以最直接的方式将它转换成一个三阶段流水线(图4-38c) , 我们将改变系统的行为。如图 4- 38c 所示 ,11 的结果成 为 14 的输入。为了通过流水线技术加速系统,我们改变了系统的行为。
Il 12 I3
时间
b ) 流水线图
I1 I 2 I3 I4
时钟
c ) 硬件: 带反馈的三阶段流水线
d ) 流水线图
图 4-38 由逻辑相关造成的流水线技术的局限性。在从未流水线化的带反馈的系统 a 转化到流水 线化的 系统 c 的 过程中,我 们改变了它的计算行为, 可以从两个流水线图Cb 和 d) 中看出来
当我们将流水线技术引入 Y86- 64 处理器时, 必 须 正 确 处 理反馈的影响。很明显,像图 4-38 中的例子那样改变系统的行为是不可接收的。我们必须以某种方式来处理指令间的数 据 和控制相关,以 使 得 到 的 行 为与 ISA 定义的模型相符。
4. 5 Y86-64 的 流水线实现
我们终于准备好要开始本章的主要任务—— 设计一个流水线化的 Y86- 64 处理器。首先 ,对 顺 序 的 SEQ 处 理 器做 一点小的改动,将 PC 的计 算挪到取指阶段。然后, 在各个阶段之间加上流水线寄存器。到这个时候, 我们的尝试还不能正确处理各种数据和控制相关 。 不 过,做 一 些 修 改 ,就 能实现我们的目标—— 一个高效的、流水线化的实现 Y86-64
ISA 的处理器。
4. 5. 1 SEQ+: 重新安排计算阶段
作为实现流水线化设计的一个过渡步骤, 我们必须稍微调整一下 SE Q 中五个阶段的顺序,使 得更新 PC 阶段在一个时钟周期开始时执行, 而不是结束时才执行。只需要对整体硬件结构做最小的改动,对 于 流水线阶段中的活动的时序,它 能 工 作 得 更 好 。 我们称这
种修改 过的设计为 " SEQ + " 。
我们移动 PC 阶段,使 得 它 的 逻辑在时钟周期开始时活动,使 它 计 算 当前指令的 PC 值。图 4-39 给出了 SEQ 和 SEQ + 在 PC 计算上的不同之处。在 SEQ 中(图 4-39a) , PC 计算发生在时 钟周期结束的时候, 根据当前时钟周期内计算出的信号值来计算 PC 寄存器的新值。在 SEQ + 中(图 4-39 b) , 我们创建状态寄存器来保存在一条指令执行过程中计算出 来的信号。然后, 当一个新的时钟周期开始时, 这些信号值通过同样的逻辑来计算当前指令的 PC。我们将这些寄存器标号为 " plc ode" 、" pCnd" 等等, 来 指 明 在 任一给定的周期, 它们保 存的是前一个周期中产生的控制信号。
PC
icode Cnd valC valM valP
a) SEQ 的新PC计算
plcodelpCndl pValM I pValC I pValP
b) S E Q +的PC选择
图 4- 39 移动计算 PC 的时间。在 S EQ + 中,我 们将计算当前状态的程序计数器的 值作为指令执行的第一步
图 4-4 0 给出了 SEQ + 硬件的一个更为详细的说明。可以看到, 其 中 的 硬件单元和控制块与我们 在 SEQ 中用到的(图4-23 ) 一样 ,只 不过 PC 逻辑从上面(在时钟周期结束时活动)移到了 下面(在时钟周期开始时活动)。
黜I S E Q + 中的 PC 在哪 里
SEQ 十有一个很 奇怪的特 色, 那就 是 没有硬件寄存器 来存放程 序 计数 器。 而是根据从 前一 条 指 令保 存 下 来 的 一 些 状 态 信 息 动 态 地 计 算 PC。 这就是 一 个 小 小 的 证
; 明一— 我们可以 以一种与 IS A 隐含着的概 念模型不 同的 方式 来 实现 处理 器 , 只要处理器能正确 执行任意的机 器语 言程序。我们不 需要将状 态编码成程序员 可见的状 态指定
;的形式 ,只 要 处理 器能 够为 任意的程序 员 可见状 态(例如 程序计数 器)产 生正 确的值。埠 创建 流水线化的设计中, 我们会 更多地 使 用到 这条原 则。 5. 7 节 中描 述的乱序 ( out of-order) 处理技术, 以一种 完全 不 同 于机 器 级 程序 中 出现的顺序的 次序 来执行指令,
(将这 一思想发挥到 了极 致。
SEQ 到 SEQ + 中对状态单元的改变是一种很通用的改进的例子, 这种改进称为电路重定时( c订cuit retimin g ) [ 68] 。重定时改变了一个系统的状态表示, 但 是 并不改变它的逻辑行为。通常用它来平衡一个流水线系统中各个阶段之间的延迟。
4. 5. 2 插入流水线寄存器
在创建一个流水线化的 Y86-64 处理器的最初尝试中, 我们要在 SEQ + 的各个阶段之间插人流水线寄存器, 并 对 信 号 重 新 排 列 ,得 到 P IP E —处 理器 , 这里的“—” 代 表 这 个处理器和最终的处理器设计相比,性 能 要 差 一 点 。 P IP E—的抽象结构如图 4-41 所 示。流水线寄 存器在该图中用黑色方框表示 , 每个寄存器包括不同的字段, 用 白 色方框表示。正 如多个字段 表明的那样, 每个流水线寄存器可以存放多个字节和字。同两个顺序处理器的硬件结构(图 4-23 和图 4-40 ) 中的圆角方框不同, 这些白色的方框表示实际的硬件组成。
访存
执行
译码
取指
图 4-40 SEQ 十的 硬件结构 。将 PC 计算从时钟周期结 束时移到了 开始时 ,使 之更适合于流水线
可以看到, P I P E —使 用了与顺序设计 SEQ ( 图 4-40 ) 几乎 一样的硬件单元, 但是有流水 线 寄 存 器分隔开这些阶段。两个系统中信号的不同之处在 4. 5. 3 节中讨论。
流水线寄存器按如下方式标号:
F 保存程序计数器的预测值,稍后讨论。
- 位于取指和译码阶段之间。它保存关千最新取出的指令的信息,即将由译码阶段进行处理。
- 位于译码和执行阶段之间。它保存关千最新译码的指令和从寄存器文件读出的值 的信息,即将由执行阶段进行处理。
M 位于执行和访存阶段之间。它保存最新执行的指令的结果, 即 将 由 访 存 阶 段 进 行处 理 。 它 还保 存关于用于处理条件转移的分支条件和分支目标的信息。
W 位于访存阶段和反馈路径之间 , 反馈路径将计算出来的值提供给寄存器文件写, 而当完成 r e t 指令时, 它还要向 PC 选择逻辑提供返回地址。
图 4-41 PIPE- 的硬件结 构,一 个初始的 流水线化实现。通过往 SEQ+ C图 4-40 ) 中插入流水 线寄存器,我们创建 了一个五阶段的流水线 。这个版本有 几个缺陷, 稍后就会解决 这些问题
图 4-42 表明以下代码序列 如何通过我们的五阶段流水线, 其 中 注 释将各条指令标识
为 Il ~ I5 以便引用:
ir movq $1,%rax # 11
2 irmovq $2,%rbx # 12
- irmovq $3, 儿r c x # 13
- irmovq $4, %r dx # I4
- h a l t # 15
2 3 4 5 6 7 8 9
图 4- 42 指令流通过 流水线的示例
图中右边给出 了这个指令序列的 流水线图 。同 4. 4 节中简单流水 线化的计算单元的流水线图一样,这 个图描述了每条指令通过流水线各个阶段的行进过程,时 间从左往右增大。上面一条数字表明各个阶段发生的时钟周期。例如, 在周期 1 取出指令 11, 然后它开始通过 流水线各个阶段,到 周期 5 结束后, 其结果写入寄存器文件。在周期 2 取出指令
12, 到周期 6 结束后, 其结果写回, 以此类推。在最下面, 我们 给出了 当周期为 5 时的流水线的扩展图 。此时, 每个流水线阶段中各有一条指令。
从图 4- 42 中还可以 判断我们画处理器的 习惯是合理的, 这样, 指令是自底向上的流动的。周期 5 时的扩展图表明的 流水线 阶段,取 指阶段在底 部, 写回阶段在最上面, 同流水线硬件图(图 4- 41 ) 表明的一样。如果看看流水线各个阶段中指令的顺序, 就会发现它们出现的顺序与在程序中列出的顺序一样。因为正常的程序是从上到下列出的,我们保留这 种顺序,让流水线从下到上进行。在使用本书附带的模拟器时,这个习惯会特别有用。
4. 5. 3 对信号进行重新排列和标号
顺序实现 SEQ 和 SEQ + 在一个时刻只处理一 条指令, 因此诸如 v a l e 、 sr c A 和 v a l E 这样的信号值有唯一的值。在流水线化的设计中, 与各个指令相关联的这些值有多个版本, 会随着指令一起流过系统。例如, 在 PIP E一的 详细结构中, 有 4 个标号为 “Sta t” 的白 色方框, 保存着 4 条不同 指令的状态码(参见图4- 41 ) 。我们需要很小心以确保使用的是正确版本的信号,否 则会有很严 重的错误,例 如将一 条指令计算出的结果存放到了另一条指令指定的目的寄存器。我们采用的命名机制,通过在信号名前面加上大写的流水线寄存
器名字作为前缀,存 储 在流水线寄存器中的信号可以唯一地被标识。例如, 4 个状态码 可以被命名为 D_s 七a t 、 E_s t a t 、M_ s t a t 和 W_s t a t 。 我们还需要引用某些在一个阶段内刚 刚计算出来的信号。它们的命名是在信号名前面加上小写的阶段名的第一个字母作为前 缀。以 状态码为例, 可以看到在取指和访存阶段中标号为 " S ta t" 的控制逻辑块。因 而, 这些块 的输出被命名为 f _s t a t 和 m_ s t a t 。 我们还可以看到整个处理器的实际状态 St a t 是根据流水线寄存器 W 中的状态值,由 写 回 阶 段中的块计算出来的。
m 信号 M _ s tat 和 m _ s tat 的差别
在命名系统中, 大写的 前缀 " D" 、 " E" 、 " M" 和 “W” 指的是流水线寄存器, 所以 M _ st at 指的是流水线寄存 器 M 的状态码 宇段。 小 写的前缀 " f" 、 " cl" 、 " e" 、 " m" 和 " w" 指的是流水 线阶段, 所以 m _ sta t 指的是在访存阶段中由控制逻辑块产 生 出的状态信 号。
理解这个命名规则对理解我们的流水线化处理器的操作是至关重要的。
SEQ十和 PIPE- 的译码阶段都产生信号 ds t E 和 ds 七M, 它 们 指明 值 va l E 和 va l M 的 目的寄存器。在 SEQ十中 , 我们可以将这些信号直接连到寄存器文件写端口的地址输入。在PIPE- 中,会在 流水线中一直携带这些信号穿过执行和访存阶段,直 到 写 回 阶段才送到寄存器文件(如各个阶段的详细描述所示)。我们这样做是为了确保写端口的地址和数据输入是来 自同一条指令。否则, 会将处于写回阶段的指令的值写入,而 寄 存 器 ID 却 来 自千处于译码阶段的指令。作为一条通用原则,我们要保存处于一个流水线阶段中的指令的所有信息。
PIPE—中有一个块在相同表示形式的 SEQ + 中是没有的, 那就是译码阶段中标号为
“Select A” 的块。我们可以看出,这个 块 会 从 来自流水线寄存器 D 的 va l P 或从 寄存 器 文件
A 端口中读出的值中选择一个,作 为流水线寄存器 E 的值 va l A。 包括这个块是为了减少要携带给流水线 寄存骈 E 和 M 的状态数量。在所有的指令中,只有 c a ll 在 访存 阶段需 要 va l P 的值。只有跳转 指令在执行阶段(当不需要进行跳转时)需要 va l P 的值。而这些指令又都不需要从寄存器文件中读出的值。因此我们合并这两个信号,将 它 们 作 为信号 va l A 携 带 穿 过流水线 ,从 而 可以减少流水线寄存器的状态数显。这样做就消除了 SEQ(图 4-23 ) 和 SEQ +
(图4-40 )中标号为 " Data" 的块, 这个块完成的是类似的功能。在硬件设计中,像 这 样 仔 细确认信号 是如何使用的,然后 通过合并信号来减少寄存器状态和线路的数量,是 很 常见 的 。
如图 4- 41 所示,我 们的流水线寄存器包括一个状态码 s t a t 字段,开 始 时 是 在取指阶段计算出来的,在访存阶段有可能会被修改。在讲完正常指令执行的实现之后,我们会在
4. 5. 6 节中讨论如何实现异常事件的处理。到目前为止我们可以说,最 系统的方法就是让与每条指令关联的状态码与指令一起通过流水线,就像图中表明的那样。
4. 5. 4 预 测 下 一 个 PC
在 PIPE- 设计中, 我们采取了一些措施来正确处理控制相关。流水线化设计的目的就是每个时钟周期都发射一条新指令,也就是说每个时钟周期都有一条新指令进入执行阶段并 最终完成。要是达到这个目的也就意味着吞吐量是每个时钟周期一条指令。要做到这一点, 我们必须在取出当前指令之后,马上确定下一条指令的位置。不幸的是,如果取出的指令是 条件分支指令,要到几个周期后,也就是指令通过执行阶段之后,我们才能知道是否要选择 分支。类似地,如果 取 出的 指 令 是r e 七,要 到指令通过访存阶段, 才能确定返回地址。
除了条件转移指令和r e t 以外,根 据取指阶段中计算出的信息, 我们能够确定下一条
指令的地址。对于 c a l l 和 j mp ( 无条件转移)来说,下 一条指令的地址是指令中的常数字
valC, 而对于其他指令来说就是 va l P。因 此, 通过预测 PC 的下一个值, 在大多数情况下,我们能达到每个时钟周期发射一条新指令的目的。对大多数指令类型来说,我们的预测是完全可靠的。对条件转移来说, 我们既可以 预测选择了分支, 那么新 PC 值应为
valC, 也可以预测没有选择分支, 那么新 PC 值应为 va l P。无论哪种情况, 我们都必须以某种方式来处理预测错误的情况,因为此时已经取出并部分执行了错误的指令。我们会在 4. 5. 8 节中再讨论这个问题。
猜测分支方向并根据猜测开始取指的技术称为分支预测。实际上所有的处理器都采用 了某种形式的此类技术。对千预测是否选择分支的有效策略已经进行了广泛的研究[ 46,
2. 3 节]。有的系统花费了大量硬件来解决这个任务。 我们的设计只使用了简单的策略, 即总是预测选择了 条件分支, 因而预测 PC 的新值为 v a l e 。
田 日 其他的分 支预测策略
我们的设 计使 用总 是选择 ( always taken ) 分支的预测策略。研究表 明这个策略的成功率大约 为 60 %[ 44 , 122 ] 。相反,从 不选择 ( never taken , NT ) 策略 的成功 率大约为40 % 。稍微复杂一点的是反向选择、正向 不选择( backwa rd taken , forward not- taken , BT F NT ) 的策略 , 当分 支地址比 下一条地址低 时就预 测选择 分支, 而分 支地 址比 较高时, 就预测不 选择分支。这种策略的成功率大约 为 65 % 。这种改进 源自一 个事 实, 即循环是由后向分支结束的, 而循 环通 常会执行 多次。前向分支用 于条 件操作, 而这 种选择的可能性 较小。在 家庭作 业 4. 55 和 4. 56 中,你 可以修改 Y86-64 流水线处理 器来 实现
NT 和 BT F NT 分支预测策略。
正如我们在 3. 6. 6 节中看到的 , 分支预测错误 会极大地 降低程序的性能,因此这就促使我们在可能的 时候,要 使用条件 数据传送而不 是条件控制转移 。
我们还没有讨论预测 r e t 指令的新 PC 值。同条件转移不同 , 此时可能的返回值几乎是无限的, 因为返回地址是 位千栈顶的 字, 其内容可以是任意的。在设计中,我 们不会试图 对返回地址做任何预测。只是简单地暂停处 理新指令, 直到 r 吐 指令通过写 回阶段。在
4. 5. 8 节中,我 们将回过来讨论 这部分的实现。
m 使用栈的返回地址预测
对大多数程序 来说 , 预测返回 值很容易,因为过 程调 用和返回是成对出现的 。大多数函数调用,会返回到调用后的那条指令。高性能处理器中运用了这个属性,在取指单 元中放 入一个硬件栈, 保存过程调用指 令产 生的 返回地址。每次执行过程调用指 令时, 都将其返回 地址压入栈 中。 当取 出一 个返回指令时, 就从 这个栈 中弹出顶部 的值, 作为 \ 预测的返回值 。同分 支预测一样,在预 测错误 时必须提供 一个恢复机制, 因为 还是有调用和返回不匹配的 时候。通常, 这种预测很 可靠。这个硬件栈对程序员来说 是不可见的。
PIP E 一的 取指阶段,如 图 4-41 底部所示,负 责预测 PC 的下一个值,以 及为取指选择实际 的 PC。我们可以 看到, 标号为 " P redict PC" 的块会从 PC 增加器计算出的 val P 和取出的指令中得到的 va l e 中进行选择。这个值存放在流水线寄存器 F 中, 作为程序计数器的预测值。标号 为 " Select PC" 的块类似于 SEQ + 的 PC 选择阶段中标号为 " PC" 的块(图4-40 ) 。它从三个值中选择一个作为指 令内存的地址 : 预测的 PC , 对千到达流水线
寄存器 M 的不选择分 支的指令来说是 v a l P 的值(存储在寄存器 M_ v a l A 中), 或是当 r e t
指令到达流水线 寄存器 WC存储在 W_ v a l M) 时的返回地址的值。
4. 5. 5 流水线冒险
PIPE- 结构是创建一个流水线 化的 Y 86- 64 处理器的好开端。不过, 回 忆 4. 4. 4 节中的讨论,将流水线 技术引入一个带反馈的系统, 当相邻指令间存在相关时会导致出现问题。在完成我们的设计之前,必须解决这个问题。这些相关有两种形式: 1 ) 数据相关,下一条指令会用 到这一条指令计算出的结果; 2 ) 控制相 关 , 一条指令要确定下一条指令的位置,例如在执行跳转、调用或返回指令时。这些相关可能会导致流水线产生计算错误,称 为冒险 ( ha za rd ) 。同相关一样, 冒险也可以分为两类: 数据冒险( da t a ha za rd ) 和控制 冒险(control haza r d ) 。我们首先关心的是数据冒险, 然后再 考虑控制冒险。
图 4-4 3 描述的是 PIPE—处理器处理 pr o g l 指令序列的情况。假设在这个例子以及后面的例子 中,程序 寄存器初始时值都为 0 。这 段代码将值 10 和 3 放入程序寄存器%r d x 和
%r a x , 执行三条 n a p 指令,然后 将寄存器%r d x 加到%r a x 。 我们重点 关注两条 ir mo v q 指
令和 a d dq 指令之间的数据相关 造成的 可能 的数据冒险。图的右边是这个指令序列的流水 线图。图 中突出显示了周期 6 和 7 的流水线阶段。流水线图的下面是 周期 6 中写回活动和周期 7 中译码活动的扩展说明。在周期 7 开始以后, 两条 i r mo v q 都 已经通过写回阶段, 所以寄存器文件保 存着更新过的 %r d x 和%r a x 的 值。因 此, 当 a d d q 指令在周期 7 经过译 码阶段时 , 它可以读到源操作数的正确值。在此示例中, 两条 ir mo v q 指令和 a d d q 指令之间的数据相关 没有造成数 据冒险。
# progl | 2 | 3 | 4 | 5 | 6 | 7 8 9 10 11 | ||||
---|---|---|---|---|---|---|---|---|---|---|
OxOOO: | irmovq | $10,‘1/.rdx | I | F | D | E | M | w | ||
OxOOa: | irmovq | $3, 1/.rax | F | D | E | M | w | |||
Ox014 : | nop | F | D | E | M | w | ||||
Ox015: nop Ox016 : nop Ox017: addq %rdx,%rax Ox019: halt | F | D F | E D F | M w E M w D E M I W F 心 D E I M I w ’ ’ ;, |
图 4-43 pr ogl 的流水线化的执行,没有 特殊的流水线控制。在周 期 6 中 ,第 二个 i rmovq 将结果
写入寄存器r% ax。addq 指令在周期 7 读源操作数, 因此得到的是r% dx 和r毛 ax 的正确值
我们看到 pr o g l 通过流水线并 得到正确的结果, 因为 3 条 no p 指令在有数据相关的指令之间创造了一些延 迟。让我们来看看如果去掉这些 no p 指令会发生些 什么。图 4-44 描述的是 pr o g 2 程序的 流水线 流程 , 在两条产生寄存器%r d x 和%r a x 值的 ir rno v q 指令和以这两个寄存器作为操作数的 a d d q 指令之间有两条 n o p 指令。在这种情况下, 关键步骤发生在周期 6’ 此时 a d d q 指令从寄存器文件中读取它的操作数。该图底部是这个周期内流水线活动的扩展描述。第一个 ir rno v q 指令巳经通过了写回阶段, 因此程序寄存 器%r d x 巳经在寄存器文件中更新过了。在该周期内, 第二个 ir rno v q 指令处于写回阶段, 因此对程序寄存器%r a x 的 写 要到周期 7 开始,时 钟上升时, 才会发生。结果, 会读出%r a x 的错误值(回想一下, 我们假设 所有的寄存器的初始值为 0 ) , 因为对该寄存器的写还未发生。很明显,我们必须改进流水线让它能够正确处理这样的冒险。
# prog2
OxOOO: irmovq $10,%rdx OxOOa: irmovq $3,%rax Ox014: nop
Ox015: nop
Ox016: addq %rdx,%rax Ox018: halt
2 3 4 5 6 7 8 9 10
昙 MIW
气D E M I W
错误值
图 4-44 p ro g2 的 流水线化的执行,没 有 特 殊 的 流水线控制。直到周期 7 结 束 时 , 对寄存
器r% a x 的写才发生,所 以 addq 指 令 在译码阶段读出的是该寄存器的错误值
图 4- 45 是当 ir mo v q 指令和 a d d q 指令之间只有一条 n o p 指令, 即为程序 p r o g 3 时, 发生的 情况。现在我们必须检查周期 5 内流水线的行为, 此时 a d d q 指令通过译码阶段。不幸的是, 对寄存器%r d x 的写仍处在写回阶段, 而对寄存器%r a x 的 写 还处 在访存阶段。因此, a d d q 指令会得到两个错误的操作数。
图 4- 46 是当去掉 ir mo v q 指令和 a d d q 指令间的所有 n o p 指令, 即为程序 pr o g 4 时, 发生的情况。现在我们必须检查周期 4 内流水线的行为, 此时 a d d q 指令通过译码阶段。不幸的是, 对寄存器%r d x 的 写 仍处在访存阶段, 而执行阶段正在计算寄存器%r a x 的新值。因此, a d d q 指令的两个操作数都是不正确的。
这些例子说明 , 如果一条指令的操作数 被它前面三条指 令中的任意一条改变的话,都会出现数据冒险。之所以会出现这些冒险,是因为我们的流水线化的处理器是在译码阶段 从寄存器文件中读取指令的操作数,而要到三个周期以后,指令经过写回阶段时,才会将 指令的结果写到寄存器文件。
# prog3 2 3 4 5 6 7 8 9
j W
I M I w
M_valE = 3 M_dstE =‘l.rax
错误值
图 4- 45 pr og3 的流水线化的 执行 ,没 有 特 殊 的 流 水 线 控 制 。 在周期 5 , addq 指令从寄存器文件中读源操作数。对寄存器釭dx 的写仍处在写回阶段, 而 对 寄 存器%r a x 的 写 还在访存阶段。两个操作数 va l A 和 va l B 得 到的 都 是 错 误 值
# prog4 | 2 | 3 | 4 | 5 | 6 7 | 8 | |||
---|---|---|---|---|---|---|---|---|---|
OxOOO : | irmovq $10,%rdx | I | F | D | E | M | w | ||
OxOOa : | irmovq $3,%rax | F | D | . E,’ M w | |||||
Ox014: | addq %r dx 丛r ax | F | D · E M I W | ||||||
Ox016: | halt | 了F : D E IM I w |
e _ valE 仁 0 + 3 = 3 E_dstE = %rax
图 4- 46 pro g 4 的 流水线化的执行,没 有 特 殊的 流水线控制。在周期 4 , a ddq 指 令从 寄存 器文件中读源操作数。对寄存器 r% dx 的 写 仍 处 在访 存 阶 段 ,而执 行阶段正在计算寄存器r% ax 的新 值 。 两个操作数va l A 和 v a l B 得 到的 都 是 错 误 值
m 列举数据冒险的类型
当一 条指令 更新后 面指令会读 到 的 那 些 程 序 状 态 时, 就有 可能 出 现 冒 险。 对 于Y86- 64 来说 ,程 序 状态 包括 程序寄存 器、 程 序计数 器、 内存 、条 件码寄存 器和 状 态寄存器。 让我们来看看在提 出的 设计中每 类状 态出 现冒险 的可能性。
程序寄存器: 我们已经认 识这种冒险 了。 出现 这种冒险是 因 为寄存器文件的读写是在不同的阶段进行的, 导致不同指令之间可能出现 不希望的 相互作用。
程序计数器: 更新和读取程序计数 器之 间的 冲突导致了控制冒险。 当我 们的取指阶段逻辑在取下一 条指令之前, 正 确预测了程 序 计数 器的 新值时, 就不会 产 生冒险。预测错误 的分支和r 釭 指令需要特殊的处理,会 在 4. 5. 5 节中讨论。
内存: 对数 据 内存的 读和写都 发生在访 存阶段。在一条读内存的指令到达这个阶段之前, 前面所有要 写内存的 指令都已经完成这个阶段 了。 另外 ,在 访存阶段中写数 据的 指令和在取指阶段中读指令之间也有冲突 , 因为指 令 和数 据内存 访问的是同一个地址空间 。只有包含自我修改代码的程序才会发生这种情况,在这样的程序中,指令写内存的一部分, 过后会从中取出指 令。有些 系统有复杂的机制来检测和 避免 这种冒险, 而有 些 系统只是简单地强制要 求程序不应该使 用自我修改代码。为 了 简便 ,假 设 程序不能修 改自身,因此我们 不需要 采取特殊的措施 ,根 据在程序执行过程中对数据内存的修 改来修改指令内存。
条件码寄存器: 在执行阶段中,整 数 操作会写这 些寄存 器。 条件传送指令会在执行阶段以及条件转移会在访存阶段读这些寄存器。在 条件传送或转移到达执行阶段之前, 前面所 有的 整数操作都已经完成 这个阶段 了。 所以不会发 生冒险。
状态寄存器: 指令流经流水线的时候,会 影响程序状 态。 我们采用流水线中的 每条指令都与一个状态码相关联的机制,使得当异常发生时,处理器能够有条理地停止,就 像在 4. 5. 6 节中会讲到的那样。
这些分析表明我们只需要处理寄存器数据冒险、控制冒险,以及确保能够正确处理 异常。 当设 计一个复 杂 系统时, 这样的分类分析是很重要 的。这样做可以确认 出 系统实现中可能的 困难,还 可以指导生成 用 于检查 系统正确性的测试 程序。
用暂停来避免数据冒险
暂停 ( s ta ll ing ) 是避免冒险的一种常用技术,暂 停 时 ,处 理 器 会 停 止 流水线中一条或多条指令,直到冒险条件不再满足。让一条指令停顿在译码阶段,直到产生它的源操作数的指令通过了写回阶段, 这样我们的处理器就能避免数据冒险。这种机制的细节会在 4. 5. 8 节 中讨论 。它 对流水线控制逻辑做了一些简单的加强。图 4-47 ( p r o g 2) 和图 4-48 (prog4) 中画出了暂停的效果。(在这里的讨论中我们省略了 pr o g 3 , 因为它的运行类似于其他两个例子。)当指令 a d dq 处于译码阶段时, 流水线控制逻辑发现执行、访存或写回阶段中至少 有一 条 指令 会更 新寄存器%r d x 或 %r a x 。 处理器不会让 a ddq 指令带着不正确的结果通过 这 个阶段,而 是 会 暂 停 指 令 ,将 它阻 塞 在译码阶段,时 间 为一个周期(对pr o g 2 来说)或者三个 周 期(对 pr og 4 来说)。对所有这三个程序来说, a d d q 指 令 最终都会在周期 7 中得到两个源操作数的正确值,然后继续沿着流水线进行下去。
将 addq 指令阻塞在译码阶段时, 我们还必须将紧跟其后的 ha lt 指令阻塞在取指阶段 。通过将程序计数器保持不变就能做到这一点, 这样一来,会 不断地对 ha lt 指令进行取指,直到暂停结束。
暂停技术就是让 一组指令阻塞在它们所处的阶段, 而允许其他指令继续通过流水 线。那么在本该正常处理 a d d q 指令的阶段中, 我们该做些什么呢? 我们使用的处理方法是: 每次要把一条 指令阻塞在译码阶段 , 就在执行阶段插入一个气泡。气泡就像 一个自动产生的 no p 指令—— 它不会改变寄存器、内存、条件码或程序状态。在图 4- 4 7 和图 4- 4 8 的流 水线图中 ,白 色方框表示的就是气泡。在这些图中, 我们用一个 a d d q 指令的标号为 " D" 的方框到标 号为 " E" 的方 框之间的箭头来表示 一个流水线气泡, 这些箭头表明 ,在 执行阶段中插 入气泡是为了替代 a d d q 指令,它 本来应该经过译 码阶段进入 执行阶段。在
- 5. 8 节中 , 我们将 看到使流水线暂停以及插入气泡的详 细机制。
# prog2 OxOOO: irmovq $10,%rdx OxOOa: irmovq $3,%rax Ox014: nop Ox015: nop bubble Ox016: addlq ;r儿 dx , ¼r ax Ox018 : halt | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
I | F | D | E | M | w | |||||||
F | D | E | M | w | ||||||||
F | D | E | M | w | ||||||||
F | D | E | M | w | ||||||||
广 | E | M | w | |||||||||
F | D | D | E | M | w | |||||||
F | F | D | E | M | w l |
图 4-47 p ro g2 使用暂停的流水线化的执行。在周期 6 中对 ad dq 指令译码之后, 暂停控制逻辑发 现一个数据冒险, 它是由写回阶段中对寄存 器%r a x 未进行 的写造成的。它在执行 阶段中 插人一个气泡,并 在周期 7 中重复对指令 a ddq 的译码。实际上, 机器是 动态地插入一条 nop 指令, 得到的执行流类似于 p ro g l 的执行流(图4-43)
# prog4 2 3 4 5 6 7 8 9 10 11
图 4- 48 p r og 4 使用暂 停的流水线化的执行。在周期 4 中对 addq 指令译码 之后,暂停控制逻辑发现了对两个 源寄存器的 数据冒险 。它在执行阶段中插入一个气泡, 并在周期 5 中重复对指令 a ddq 的译码。它再次发现对 两个源寄存 器的冒险 , 就在执行阶段中插入一 个气泡, 并在周期 6 中重复对指令 a ddq 的译码。它再次发 现对寄存 器釭a x 的冒险,就在 执行阶段中插入一个气泡 , 并在周期 7 中重复对指令 addq 的译码。实际上, 机器是动态地插入 三条 no p 指令,得到的执行 流类 似于 p r og l 的执行流(图4-43)
在使用暂停技术来 解决数据冒险的过程中, 我们通过 动态地 产生和 pr o g l 流(图4- 4 3 ) 一样的 流水线流,有 效地执行了程序 pr o g 2 和 pr o g 4。为 p r o g 2 插入 1 个气泡, 为 p r o g 4 插入 3 个气泡, 与在第 2 条 ir mo v q 指令和 a d d q 指令之间有 3 条 n o p 指令, 有相同的效果。虽 然实现这一机制相当容易(参考家庭作 业 4. 53), 但是得到的性能并不很好。一条指令更新一个寄存器,紧跟其后的指令就使用被更新的寄存器,像这样的情况不胜枚举。这会导致流水线暂停长达三个周期,严重降低了整体的吞吐量。
用转发来避免数据冒险
PIPE - 的设计是在译码阶段从寄存器文件中读入源操作数 , 但是对这些源寄存器的写有可能要在写回阶段才能进行。与其暂停直到写完成,不如简单地将要写的值传到流水线寄存 器 E 作为源操作数 。图 4-49 用 pr og 2 周期 6 的流水线图 的扩展描述来说明 了这一策略。译码阶段逻辑发现 , 寄存器%r a x 是操作数 v a l B 的源寄存器 , 而在写端口 E 上还有一个对 %r a x 的未进行的写。它只要简单地将提供到端口 E 的数据字(信号 W_ va l E) 作为操作数 v a l B 的值,就能避免暂停。这种将结果值直接从一个流水线阶段传到较早阶段的技术称为数据转发(data forwarding, 或简称转发, 有时称为 旁路( bypassing ) ) 。它使得pr og 2 的指令能通过流水线而不需要任何暂停。数据转发需要在基本的硬件结构中增加一些额外的数据连接和控制逻辑。
# prog2 10
OxOOO: irmovq $10丛r dx w
OxOOa: irmovq $3,%rax M
Ox014: nop w
Ox015: nop M w
Ox016: addq %rdx,%rax M w
Ox018: halt w
图 4- 4 9 pr og2 使用 转发的 流水线化的执行。在周期 6 中,译 码阶段逻辑发现有在写回 阶段中
对寄存器r% ax 未进行的写。它用这个 值, 而不是从寄存器文件中读出的值, 作为源
操作数 va l B
如图 4- 50 所示, 当访存阶段中有对寄存器未进行的写时,也 可以使用数据转发, 以避免程 序 p r o g 3 中的暂停。在周期 5 中, 译码阶段逻辑发 现, 在写回阶段中端口 E 上有对寄存器%r d x 未进行的 写, 以及在访存阶段中有会在端口 E 上对寄存器%r a x 未进行的写。它不会暂停直到这些写真正发生,而 是用写回阶段中的值(信号 W_ v a l E) 作为操作数 va
lA, 用访存阶段中的值(信号 M_ v a l E) 作为操作数 v a l B。
为了充分利用数据转发技术,我们还可以将新计算出来的值从执行阶段传到译码阶段, 以避免程序 pr o g 4 所需要的暂停,如图 4-51 所示。在周期 4 中, 译码阶段逻辑发现在访存阶段中有对寄存器 %r dx 未进行的写,而 且执行阶段中 ALU 正在计算的值稍后也会写入寄存器%r a x 。 它 可以将访存阶段中的 值(信号M_ v a l E) 作为操作数 v a l A, 也可以将 ALU 的输出
(信号 e _v a l E) 作为操作数va l B。注意, 使用 ALU 的输出不会造成任何时序间题 。译码阶段只要在时钟周期结束之前产生信号 va l A 和 va l B, 这样在时钟上升开始下一个周期时,流水线寄存器 E 就能装载来自译码阶段的值了。而在此之前 ALU 的输出巳经是合法的了。
# prog3 2 3 4 5 6 7 8 9
图 4-50 pr og 3 使用转发的流水线化的执行。在周期 5 中,译 码阶段逻辑发现有在写回阶段中对寄存器
%r d x 未进行的写 , 以 及 在访存阶段中对寄存器%r a x 未进行的写。它用这些值, 而不是从寄存器文件中读出的值,作 为 v a l A 和 va l B 的 值
# prog4 2 3 4 5 6 7 8
OxOOO: irmovq $10,%rdx 厂户
OxOOa : irmovq $3,%rax
Ox014: addq %rdx,%rax I F w
Ox016: halt s t) n i= M I W
图 4-51 pro g 4 使 用 转发的 流水线化的执行。在周期 4 中,译 码 阶 段 逻辑 发现有在访存阶段中对寄存器%r d x 未进行的写, 还发现在执行阶段中正在计算寄存器% r a x 的 新 值 。 它 用 这些值,而 不 是 从 寄存 器文件中读出的值,作 为 va l A 和 va l B 的值
程序 pr o g 2 ~ pr o g 4 中描述的转发技术的使用都是将 ALU 产生的以及其目标为写端 口 E 的值进行转发,其 实 也 可以 转发从内存中读出的以及其目标为写端口 M 的值。从访存阶段,我 们可以转发刚刚从数据内存中读出的值(信号 m_val M) 。从 写回阶段 , 我们可以转发对 端口 M 未进行的写(信号W_v al M)。这样一共就有五个不同的转发源Ce —v al E 、m v al M、M_v al E、W_v al M 和 W_v al E) , 以 及 两个 不同 的转 发 目的 ( v al A 和 v a l B) 。
W_valE
valE valM dstE IdstMI 屯
1W_valM
m_valM
访存
M_Cnd M_valA
stat licode’ ~
执行
勺 , 1 stat licodel ifun
W_valM W_valE
ifun I rA I rB
指令内存
M_valA W_valM
图 4-52 流水线化的最终 实现一 PIPE 的硬件结构。添加的旁 路路径能够转发前 面三条指令的结果。这使得我们能够不暂停流水线 就处理大多数形式的 数据冒险
图 4-49~ 图 4-51 的扩展图还表明译码阶段逻辑能够确定是使用来自寄 存器 文件的值, 还是要用转发过来的值。与每个要写回寄存器文件的值相关的是目的寄存器 ID 。逻辑会将这些 ID 与源寄存器 ID sr c A 和 sr c B 相比较,以 此来检测是否需要转发。可能有多个目的寄存 器 ID 与一个源 ID 相等。要解决这样的情况, 我们必须在各个转发源中建立起优先级关系。 在学习转发逻辑的详细设计时, 我们会讨论这个内容。
图 4- 52 给出的是 P IP E 的结构, 它 是 P IP E — 的扩展,能 通过转发处理数据冒险。将这幅图与 P IP E 一的 结构(图 4- 41 ) 相比, 我们可以看到来自五个转发源的值反馈到译码阶段中两个标号为 " Sel + F w d A" 和 " F w d B" 的 块。标号为 " S e l + F w d A" 的 块 是 P IP E — 中标号为 " S elect A " 的块的功能 与转发逻辑的结合。它允许流水线寄存器 E 的 v a l A 为 巳增加的 程序计数器值 v a l P, 从寄存器文件 A 端口读出的值, 或者某个转发过来的值。标号为 " F w d B" 的块实现的是源操作数 v a l B 的转发逻辑。
加载/使用数据冒险
有一类 数据冒险不能单纯用转发来解决,因 为内存读在流水线发生的比较晚。图 4-53 举例说明了加 栽/使 用冒险 Cload / use hazard) , 其中一条指令(位于地址 Ox 028 的 mrmovq ) 从 内 存中读出寄存器%r a x 的 值 ,而 下 一 条 指 令(位于地址 Ox 0 32 的 a d d q ) 需 要 该 值 作 为源操作数。图的下部是 周期 7 和 8 的扩展说明, 在此假设所有的程序寄存器都初始化为 0。a d d q 指令在周期 7 中需要该寄存器的值,但 是 mrmovq 指令直到周期 8 才产生出这个值。为了从 mr mo vq “转发到" addq, 转发逻辑不得不将值送回到过去的时间!这显然是不可能的,我们必须找到其他 机制来解决这种形式的数据冒险。(位于地址 Ox Ol e 的 i r mo vq 指 令产 生的寄存器%r b x 的值,会被位 于地址 Ox 032 的 a ddq 指 令 使用 , 转发能够处理这种数据冒险。)
# prog5 2 3 4 5 6 7 8 9 10 11
图 4-53 加载/使用数据冒险的示例。addq 指令在周期 7 译码阶段中需要寄存器%r a x 的值。前 面的
mr mo v q 指 令 在 周 期 8 访 存 阶段 中 读出这个寄存器的新值, 这对千 addq 指令来说太迟了
如图 4-54 所示, 我们可以将暂停和转发结合起来, 避免加载/使用数据冒险 。这个需要修改控制逻辑 , 但是可以使用现有的旁路路径。当 mr mo v q 指令通过执行阶段时,流水 线控制逻辑发现译码阶段中的指令( a d d q ) 需要从内存中读出的结果。它会将译码阶段中的指令暂停一个周期,导 致执行阶段中插入一个气泡。如周期 8 的扩展说明所示 ,从 内存中读出的值可以从 访存阶段转发到译码阶段中的 a d d q 指令。寄存器%r b x 的值也可以从 访存阶段转发到译码阶段。就像流水线图 ,从 周 期 7 中标号为 " D" 的方框到周期 8 中标号为" E" 的方框的箭头表明的那样, 插入的 气泡代替了正常情况下本 来应该继续通过流水 线的 a d d q 指令。
# prog5 | 2 | 3 | 4 | 5 | 6 7 8 9 10 11 12 | ||
---|---|---|---|---|---|---|---|
OxOOO: | irmovq $128,%rdx | F | D | E | M | w | |
OxOOa: | irmovq $3,%rcx | F | D | E | M | w | |
Ox014 : | rmmovq %rcx, 0(%rdx) | F | D | E | |||
Ox01e : | irmovq $10,%rbx | F | D | E | |||
Ox028: | mrmovq 0(%rdx),%rax | # Load | o/.rax | F | D |
bubble E M w
Ox032 : addq %rbx,r; 人 ax # Use %rax Ox034: halt
F I E
I M Iw
图 4-54 用暂停来处 理加载/使用冒险。通过将 a ddq 指令在译码阶段暂停一个周期 , 就可以将 va lB
的值从访存阶段中的 mr movq 指令转发到译码 阶段中的 a ddq 指令
这种用暂停来处理加载/使用冒险的方法称为加载互 锁 Clo a d in t e rl o ck ) 。加载互锁和转发技术结合起来足以处理所有可能类型的数据冒险。因为只有加载互锁会降低流水线的 吞吐量,我 们几乎可以 实现每个时钟周期发 射一条新指令的吞吐量目标。
避免控制冒险
当处理器无法根据处于取指阶段的当前指令来确定下一条指令的地址时,就会出现控 制冒险。如同 在 4. 5. 4 节讨论过的, 在我们的流水 线化处理器中, 控制冒险只会发生在
r e t 指令和跳转指 令。而且, 后一种情况只有在条件跳转方向预测错误时才会造成麻烦。在本小节中, 我们概括介 绍如何来处理这些冒险。作为对流水线控制更一般性讨论的一部
分, 其详细实现将在 4. 5. 8 节给出。
对于r e t 指令, 考虑下面的示例程序。这个程序是用汇编代码表示的 ,左 边是各个指令的地址,以供参考:
OxOOO: irmovq stack,%rsp # OxOOa: call proc # Ox013: irmovq $10,%rdx # Ox01d: halt
Ox020 : . pos Ox20
Initialize stack pointer Procedure call
Return point
Ox020: proc: Ox020 : ret
Ox021: rrmovq %rdx, %rbx Ox030: . pos Ox30
Ox030: stack:
# proc:
# Return immediately
# Not executed
# stack: Stack pointer
图 4-55 给出了 我们希望流水线如何来处理r e t 指令。同前面的 流水线图一样, 这幅图展示了 流水线的活动, 时间从左向右增加。与前面不同的 是, 指令列出的顺序与它们在程序中出现的顺序并不相同,这是因为这个程序的控制流中指令并不是按线性顺序执行 的。看看指令的地 址就能看出它们在程序中的位置。
# prog7
OxOOO: irmovq Stack,%edx OxOOa: call proc
Ox020: ret
bubble bubble bubble
Ox013: irmovq $10,%rdx # Return point
2 3 4 5 6 7 8 9 10 11
图 4-5 5 r e t 指令处理的简化视图 。当 r e t 经过译码 、执行和访存阶段 时,流 水线应该暂停,在处 理过程中插人三个气泡。一旦ret 指令到达写回阶段(周期7) , PC选择逻辑就会选择返回地址作 为指令的取指地址
如这张 图所示 , 在周期 3 中取出r e t 指令, 并沿着流水线前 进,在周期 7 进入写回阶段。在 它经过译码、执行和访存阶段时, 流水线不能做任何有用的活动。我们只能在流水 线中插入三 个气泡。一旦r e 七指令到达写回阶段, P C 选择逻辑 就会将程序计数器设 为返回地址, 然后取指阶段就会取出位于返回点(地址Ox 013 ) 处的 ir mo v q 指令。
要处理预测错误的分支,考虑下面这个用汇编代码表示的程序,左边是各个指令的地 址, 以供参考:
OxOOO: Ox002:
OxOOb: Ox015: Ox016: Ox016: Ox020:
Ox02a:
xorq %rax,%rax jne target irmovq $1, %rax halt
target:
irmovq $2, %rdx irmovq $3, %rbx halt
# Not taken
# Fall through
# Target
# Target+!
图 4-56 表明是如何处理这些指令的。同前面一样, 指令是按照它们进入流水线的顺
序列出的,而不是按照它们出现在程序中的顺序。因为预测跳转指令会选择分支,所以周期 3 中会取出位于跳转目标处的指令, 而周期 4 中会取出该 指令后的那条指令。在周期 4, 分支逻辑发现不应该选择分支之前,已经取出了两条指令,它们不应该继续执行下去了。幸运的是,这两条指令都没有导致程序员可见的状态发生改变。只有到指令到达执行阶段时才会发 生那种情况, 在执行阶段中, 指令会改变条件码 。我们只要在下一个周期往译码和执行阶段中插入气泡,并同时取出跳转指令后面的指令,这样就能取消(有时也称为指令排除( in s t ru c t io n s q u a s h in g ) ) 那两条预 测错误的指令。这样一来, 两条预测错误的指令就会简单地从流水线中消失,因此不会对程序员可见的状态产生影响。唯一的缺点是两个时钟周期的指令处理能力被浪费了。
# prog7
OxOOO: xorq 1r儿
ax , %r ax
2 3 4 5 6 7 8 9 10
F I D I E I M I W
Ox002: jne target # Not taken Ox016: irmovl $2,:r人 d x # Target
bubble
Ox020: irmovl $3,%rbx # Target+1 bubble
OxOOb: irmovq $1,%rax # Fall through Ox015: halt
F I I I M Iw
E I M I W
三 l w
图 4-56 处理预测错误的分支指令。流水线预测会选择分支,所以开始取跳转目标处的指令。在周期 4 发现预测错误 之前, 已经取出了两条指令, 此时, 跳转指令正在通过执行阶段。在周期 5 中, 流水线往译码和执行阶段 中插入气泡, 取消了两条目标指令, 同时还取出跳转后面的那条指令
对控制冒险的讨论表明,通过慎重考虑流水线的控制逻辑,控制冒险是可以被处理 的。在出现特殊情 况时 ,暂停 和往流水 线中插入气泡的技术可以 动态调整流水 线的流程。如同我们将在 4. 5. 8 节中讨论的一样 , 对基本时钟寄 存器设计的简单扩展就可以让我们暂停流水段,并向作为流水线控制逻辑一部分的流水线寄存器中插入气泡。
4. 5. 6 异常处理
正如第 8 章中将讨论的, 处理器中很多事情都 会导致异常控制流, 此时, 程序执行的正常流程被破 坏掉。异常可以由程序执行从内部产生,也 可以由某个外部信号从外部产生。我们的指令集体系结构包括三种不同的内部产生的异常: 1) h a lt 指令, 2 ) 有非法指令 和功能码组合的指令, 3 ) 取指或数据读写试图访问一个非法地址。一个更完整的处理器设计应该也能处理外部异常,例如当处理器收到一个网络接口收到新包的信号,或是一个 用户点击鼠标按钮的信号。正确处理异常是任何微处理器设计中很有挑战性的一方面。异 常可能出现在不可预测的时间,需要明确地中断通过处理器流水线的指令流。我们对这三 种内部异常的处理只是让你对正确发现和处理异常的真实复杂性略有了解。
我们把导致异 常的指令称为异常指 令( e x c e p t in g in s t ru c t io n ) 。 在使用非法指令地址的情况中, 没有实际的异常指令, 但是想象在非法地址处有一种“虚拟指令” 会有所帮助。在简化的 ISA 模型中, 我们希望 当处理器遇到异常时 , 会停止, 设置适当的状态码,如图
4-5 所示。看上去应该是到异常指令之前的所有指令都已经完成 , 而其后 的指令都不应该对程序员可见的状态产生任何影响 。在一个更完整的设 计中, 处理器会继续调用异常处理
程序 ( e xce pt io n handler), 这是操作系统的一部分,但是实现异常处理的这部分超出了本书讲述的范围。
在一个流水线化的系统中,异常处理包括一些细节问题。首先,可能同时有多条指 令会引 起异常。例如, 在一个流水线操作的周期内,取 指阶段中有 h a lt 指令, 而数据内存会报告访存阶段中的指令数据地址越界。我们必须确定处理器应该向操作系统报告 哪个异常。基本原则是:由流水线中最深的指令引起的异常,优先级最高。在上面那个 例子中,应该报告访存阶段中指令的地址越界。就机器语言程序来说,访存阶段中的指 令本来应该在取指阶段中的指令开始之前就结束的,所以,只应该向操作系统报告这个 异常。
第二个细节问题是 , 当首先取出一条指令 , 开始执行 时, 导致了一个异常, 而后来由于分支预测错误,取消了该指令。下面就是一个程序示例的目标代码:
OxOOO: 6300 xorq %rax,%rax
Ox002: 741600000000000000 I jne target # Not taken
OxOOb: 30f00100000000000000 I irmovq $1, 1r儿
Ox015: 00 I halt
Ox016: I target:
ax # Fall through
Ox016: ff .byte OxFF # Invalid instruction code
在这个程序中 , 流水线会预测选择分支, 因此它会取出并以一个值为 Ox FF 的字节作为指令(由汇编代码中 . b y t e 伪指令产生的)。译码阶段 会因此发现一个非法指令异常。稍后,流水线会 发现不应该选 择分支, 因此根本就不应该取出位于地址 Ox 01 6 的指令。流水线控制逻辑会取消该指令,但是我们想要避免出现异常。
第三个细节问题的产生是因为流水线化的处理器会在不同的阶段更新系统状态的不同部分。有可能会出现这样的情况,一条指令导致了一个异常,它后面的指令在异常指令完 成之前改 变了部分状态。比如说 , 考虑下面的代码序列, 其中假设不允许用户程序访问 64 位范围的高端地址:
irmovq $1,%rax
xorq %rsp,%rsp pushq %rax
addq %rax,%rax
# Set stack pointer to O and CC to 100
# Attempt to write to Oxfffffffffffffff8
# (Should not be executed) Would set CC to 000
pus hq 指令导致一个地址异常, 因为减小 栈指针会导致它绕回到 Ox f f f f f f f f f f f f f f f8 。访存阶段中会发 现这个异常。在同一周期中, a d d q 指令处于执行阶段, 而它会将条件码 设置成新的值 。这就会违反异常指令之后的所有指令都不能 影响系统状态的要求 。
一般地 , 通过在流水线结构中加入异常处理逻辑 , 我们既能 够从各个异常中做出正确的选择,也能 够避免出 现由千分 支预测 错误取出的指令造成的异常。这就是为什么我们会在每个流水 线寄存器中包括一个状态码 s t a 七(图 4-41 和图 4- 52 ) 。如果一条指令在其处理中于某个阶段 产生了一个异常, 这个状态字段就被设置成指示异常的种类。异常状态和该指令的其他信息一起沿着流水线传播,直到它到达写回阶段。在此,流水线控制逻辑发现 出现了异常,并停止执行。
为了避免异 常指令之后的指令更新任何程序员可见的状 态, 当处千访存或写回阶段中的指令导致 异常时 , 流水线控制逻辑必须禁 止更新条件码寄存器或是数据内存。在上 面的示例程序 中, 控制逻辑会发现访存阶段中的 p u s hq 导致了异常, 因此应该禁止 a d d q 指令更新条件码寄存器。
让我们来看看这种处理异常的方法是怎样解决刚才提到的那些细节问题的。当流水线 中有一个或多个阶段出现异常时,信息只是简单地存放在流水线寄存器的状态字段中。异 常事件不会对流水线中的指令流有任何影响,除了会禁止流水线中后面的指令更新程序员 可见的状态(条件码寄存器和内存),直到异常指令到达最后的流水线阶段。因为指令到达写回阶段的顺序与它们在非流水线化的处理器中执行的顺序相同,所以我们可以保证第一 条遇到异常的指令会第一个到达写回阶段,此 时程序执行会停止, 流水 线寄存器 W 中的状态码会被记录为程序状态。如果取出了某条指令,过后又取消了,那么所有关于这条指 令的异常状态信息也都会被取消。所有导致异常的指令后面的指令都不能改变程序员可见 的状态。携带指令的异常状态以及所有其他信息通过流水线的简单原则是处理异常的简单 而可靠的机制。
5 . 7 P IPE 各 阶 段 的 实现
现在我们已 经创建了 PIPE 的整体结构, PIP E 是我们使用了转发技术的流水线化的Y8 6- 64 处理器。它使用了一组与前 面顺 序设 计相同的硬件单元, 另外增加了一些流水线寄存器、一些重新配置了的逻辑块,以及增加的流水线控制逻辑。在本节中,我们将浏览 各个逻辑块的设计,而 将流水 线控制逻辑的设计放到下一节中介绍。许多逻辑块与’ SEQ 和 SEQ+ 中相应部件完全相同, 除了我们必须 从来自不 同流水线寄存器(用大写的流水线寄存器的名字作为前缀)或来自各个阶段计算(用小写的阶段名字的第一个字母作为前缀) 的信号中选择适当的值。
作为一个示 例,比较 一下 SEQ 中产生 sr c A 信号 的 逻辑的 HCL 代码与 PIPE 中相应的代码:
# Code from SEQ
word srcA = [
icode in { IRRMOVQ, IRMMOVQ, IOPQ, IPUSHQ } : rA; icode in { IPOPQ, IRET} : RRSP;
1 : RNONE; # Don’t need register
];
# Code from PIPE
word d_srcA = [
D_icode in { IRRMOVQ, IRMMOVQ, IOPQ, IPUSHQ } : D_rA; D_icode in { IPOPQ, IRET} : RRSP;
1 : RNONE; # Don’t need register
];
它们的不同之处只在于 PIPE 信号都加上了前缀: " D_ " 表示源值 , 以表明信号是来自流水线 寄存器 D , 而 " ct_" 表示结果值, 以表明它是在译码阶段中产生的。为了避免重复, 我们在此就不列出那些与 SEQ 中代码只有名字前缀不同的块的 HCL 代码。网 络旁注ARCH : HCL 中列出了完整的 PIPE 的 HCL 代码。
PC 选择和取指阶段
图 4-57 提供了 PIPE 取指阶段逻辑的一 个详细描述。像前面讨论过的那样, 这个阶段必 须选择程序计数 器的当前值 , 并且预测下一个 PC 值。用于从内存中读取指令和抽取不同指令字段的 硬件单元与 SEQ 中考虑的那些一样(参见4. 3. 4 节中的取指阶段)。
溺 s tat licodel ifun I rA I rB
valC 陑
M_icode
M_Cnd
j M_valA
j I W_icode
. : Need
, ……
..勺., • ;. i
, _:: PC 、 :
::
- 令 i
valC ;
PC j
- Need ’ 增加 i
re gids;;.-," i
厂令内存罕 气 , I I
亏 L I
图 4-5 7 PI PE 的 PC 选择 和取指逻辑。在一个周期的时间限 制内 , 处理器只能预测下 一条指令的 地址
PC 选择逻辑从 三个程序计数 器源中 进行选择。当一条预测错误的分支进入访存阶段时, 会从流水线寄存器 M ( 信号 M_ v a l A) 中读出该指令 v a l P 的值(指明下一条指令的地址)? 当r 吐 指令进入写 回阶段时, 会从流水线寄存器 W ( 信号 W_ v a l M) 中读出返回地址。其他情况会使用存 放在流水线 寄存器 F ( 信号 F_p r e d PC) 中的 P C 的预测值:
word f_pc = [
# Mispre’.iicted branch. Fetch at incremented PC M_icode == IJXX && !M_Cnd: M_valA;
# Completion of RET instruction W_icode == IRET: W_valM;
# Default: Use predicted value of PC
1 : F_predPC;
];
当取出的 指令为函数调用或跳转时 , P C 预测逻辑会选择 v a l e , 否则就会选择 v a l P: word f_predPC = [
f_icode in { IJXX, ICALL} : f_valC;
1 : f_valP;
];
标号为 " I n s t r valid " 、" N eed r egid s" 和 " N eed va!C" 的逻辑块和 SEQ 中的一样,使用了适当命名的源信号。
同 SEQ 中不一样,我们 必须 将指令状态的计算分成两个部分。在取指阶段, 可以测试由千指令 地址越界引 起的内存错误,还 可以发现非法指令或 h a lt 指令。必须推迟到访
存阶段才能发现非法数据地址。
练习题 4. 30 写 出信号 f _ s t a t 的 H CL 代码 , 提供取出的 指令的临 时状 态。
译码和写回阶段
图 4-58 是 PIP E 的译码和写回逻辑的详细说明。标号为 " cts t E " 、 " cts t M" 、 " s r c A" 和 " sr c B " 的块非常类似千它们在 SEQ 的实现中的相应部件。我们观察到, 提供给写端口的寄存器 ID 来自于写回阶段(信号 W_ d s t E 和 W_ d s t M) , 而不是来自于译码阶段。这是因为我们希望进行写的目的寄存器是由写回阶段中的指令指定的。
e_dstE
图 4-58 PIPE 的译码和写回阶段逻辑。没有指令既需要 val P 又需要来自寄存器端口 A 中读出的值,因此对后面的阶段来说 , 这两者可以合并为信号 val A。标号为 " Sel + F wd A " 的块执行该任 务,并实现源操作数 val A 的转发逻辑。标号为 " F wd B" 的块实现源操作数 val B 的转发逻辑。寄存器写的位置是由 来自写回 阶段的 d s t E 和 ds t M 信号指定 的, 而不是来自千译 码阶段, 因为它 要写的是当前正在写回阶段中的指令的结果
练习题 4. 31 译码 阶段中标号为 " d s t E " 的块根据来 自 流水线寄 存器 D 中 取出的指令的各 个 字 段, 产生寄存器文件 E 端 口 的寄存器 ID。在 PIP E 的 H CL 描述 中, 得到的信号命名 为 d _ d s t E 。 根据 S EQ 信号 d s 七E 的 H CL 描述, 写 出这 个信号 的 H CL 代码。(参考 4. 3. 4 节 中的译码 阶段。)目前还不 用 关心 实现 条件传 送的逻辑。
这个阶段的复杂性主要是跟转发逻辑相关。就像前面提到的那样,标 号 为 " Sel + Fwd
A" 的块扮演两个角色。它为后 面的阶段将 v a l P 信号合并到 v a l A 信号,这 样 可以减少流水 线 寄存器中状态的数量。它还实现了源操作数 v a l A 的转发逻辑。
合并信号 v a l A 和 v a l P 的依据是,只 有 c a l l 和跳转指令在后面的 阶段中需要 v a l P 的值, 而这些指令并不需要从寄存器文件 A 端口中读出的值。这个选择是由该阶段的 i code 信号来控制的。当信号 D_ i c o d e 与 c a l l 或 j XX 的 指令代码相匹配时 , 这个块就会选择 D_ v a l P 作为它 的输出。
- 5. 5 节中提到有 5 个不同的转发源, 每个都有一个数 据字和一个目的寄存器 ID :
数据字 | 寄存器 ID | 源描述 |
---|---|---|
e val E | e ds t E | ALU 输 出 |
m val M | M ds t M | 内 存 输 出 |
M val E | M dstE | 访存阶段中对端口 E 未进 行 的 写 |
W val M | W dstM | 写 回阶 段中 对 端 口 M 未进行的写 |
W val E | W dstE | 写回阶段中对端口 E 未 进 行的 写 |
如果不满足任何 转发条件, 这个块就应该选择 d—r v a l A 作为它的输出,也 就是从寄存器端 口 A 中读出的值。
综上所述 , 我们得 到以下流水线寄存器 E 的 v a l A 新值的 H CL 描述:
word d_val A = [
D_icode in { ICALL, IJXX} : D_valP; # Use incremented PC d_srcA == e_dstE: e_valE; # Forward valE from execute d_srcA == M_dstM: m_valM; # Forward valM from memory d_srcA == M_dstE: M_valE; # Forward valE from memory d_srcA == W_dstM: W_valM; # Forward valM from write back d_srcA == W_dstE: W_va l E; # Forward valE from write back
1 : d_rvalA; # Use value read from register file
];
上述 H CL 代码中赋予这 5 个转发源的优先 级是非常重要的。这种优先级是由 HCL 代码中检测 5 个目的寄存器 ID 的顺序来确定的。如果选择了其他任何顺序, 对某些程序来说, 流水线就会出错。图 4-59 给出了一个程序示例,要 求对执行和访存阶段中的转发源设置正确的优先级。在这个程序中 , 前两条指令写寄存器%r dx , 而第三条指令用这个寄存器作为它的 惊操作数。当指令r r mo vq 在周期 4 到达译码阶段时, 转发逻辑必须在两个都以该 源寄存 器为目的的值中选择一个。它应该选择哪一个呢?为了设定优先级,我们必须考虑当一次执行一条指令时 , 机器语言程序 的行为。第一条 i rmovq 指令会将寄存器%r dx 设 为 10 , 第二条 i rmovq 指令会将之设为 3, 然后r rmovq 指令会从%r dx 中读出 3。为了模 拟这种行为,流 水线化的实现应 该总是给处于最早流水线阶段中的转 发源以较高的优先级, 因为它保 持着程序序列中设置该寄存器的最近的指令。因此,上述 H CL 代码中的逻辑首先会检测执行阶段中的转发源, 然后是访存阶段 , 最后才是写回阶段。只有指令 pop q %r s p 会关心在访存或写回阶段中的两个源之间的转发优先 级, 因为只有这条指令能同时写两个寄存器。
可 练习题 4. 32 假设 d v a l A 的 H C L 代码中第三和 第四种 情况(来 自 访存阶段的 两个 转发源)的顺序是 反过来的。 请描 述下列程序中 r r mo v q 指令(第5 行)造成的行 为 :
irmovq $5, %rdx irmovq $0x100,%rsp rmmovq %rdx,0(%rsp) popq %rsp
rrmovq %rsp,%rax
# prog8 2 3 4 5 6 7 8
OxOOO: irmovq $10,%rdx I F I D OxOOa: irmovq $3,%rdx
Ox014: rrmovq %rdx,%rax Ox016: halt
归l w
图 4-59 转发优先级的说明。在周 期 4 中,%r dx 的 值既可以从执行阶段也可以从访存阶段得到. 转发逻辑应该选择执行阶段中的值,因为它代表最近产生的该寄存器的值
练习题 4 . 33 假设 d _ v a l A的 HCL 代码中第五和第 六种情况(来自写 回 阶段的 两个转发源)的顺序是反过来的。写 出 一个 会运行错误的 Y86-64 程 序。 请描述错误 是如何发生的,以及它对程序行为的影响。
练习题 4 . 34 根据提供到 流水线寄存器 E 的源操作数 v a l B 的值, 写 出 信号 d _ v a lB
的 HCL 代码。
写 回 阶 段 的 一 小 部 分 是 保 持 不 变 的 。 如图 4- 5 2 所示, 整 个 处 理 器的状态 St a t 是一个 块 根据流水线寄存器 W 中的状态值计算出来的。回想一下 4. 1. 1 节 , 状 态 码 应该指明 是 正 常 操 作 ( AOK) , 还是三种异常条件中的一种。由于流水线寄存器 W 保存着最近完成的指令的状态,很自然地要用这个值来表示整个处理器状态。唯一要考虑的特殊情况 是当写回阶段有气泡时。这是正常操作的一部分,因此对于这种情况,我们也希望状态 码是 AOK:
word Stat = [
W_stat == SBUB: SAOK;
1 : W_stat;
];
3 . 执行阶段
图 4-60 展现的是 PIPE 执行阶段的逻辑。这些硬件单元和逻辑块同 SEQ 中的相同, 使 用 的 信 号 做 适当的重命名。我们可以看到信号 e —v a l E 和 e _ d s t E 作为转发源, 指向译 码 阶 段 。 一 个 区 别 是 标 号 为 " Se t CC" 的逻辑以信号 m_ s 七a t 和 W_ s t a t 作 为输入, 这个; 逻辑决定了是否要更新条件码。这些信号被用来检查一条导致异常的指令正在通过后面的 ' 流水线阶段的情况,因 此, 任 何 对 条 件 码 的 更 新 都 会 被 禁止。这部分设计在 4. 5. 8 节中; 讨论。
e_valE e_dstE
图 4-60 PIPE 的执行阶段逻辑 。这一部分的设 计与 SEQ 实现中的 逻辑非常相似
练习题 4. 35 d _ va l A 的 HCL 代码 中的 第 二种 情况 使用 了 信号 e _d s t E , 来判断是否要选 择 ALU 的输出 e _v a l E 作为 转发源。 假设我们 用 E_ d s t E , 也就是流水线寄存器
E 中的 目的寄存器 ID , 来作为这个选择。写出一个采用这个修改过的转发逻辑就会产生错 误结果的 Y86-64 程序。
4 访存阶段
图 4-61 是 P IP E 的 访 存阶段逻辑。将这个逻辑与 S E Q 的访存阶段(图4-30 ) 相比较 ,我们看到,正 如 前 面提到的那样, P IP E 中 没 有 SEQ 中标号为 " Da ta" 的块。这个块是用来在数 据源 v a l P( 对 c a l l 指令来说)和v a l A 中 进 行 选择的, 但 是 这个选择现在由译码阶段中标 号为 " S el + Fwd A" 的块来执行。这个阶段中的其他块都和 SEQ 中相应的部件相同,采 用的信号做适当的重命名。在图中,你 还 可以看到许多流水线寄存器 M 和 W 中的值作为转发和流水线控制逻辑的一部分,提供给电路中其他部分。
W_valE W_valM W_dstE W_ds!M
m_valM
M_dstE M_dstM
M_valA M_valE
图 4-61 PIPE 的访存 阶段逻辑。许多从流水线寄存器 M 和 W 来的信号被传递到较早的阶段, 以提供写回的结果、指令地址以及转发的结果
练习题 4. 36 在这个阶段中,通过检查数据内存的非法地址情况,我们能够完成状 态 码 S t a t 的计算。 写 出信号 m_ s t a 七的 H CL 代码。
5. 8 流水线控制逻辑
现在准备创建流水线控 制逻辑, 完成我 们的 PIP E 设计。这个逻辑必须处理下面 4 种控制情况,这些情况是其他机制(例如数据转发和分支预测)不能处理的:
加载/使用冒险: 在一条从内存 中读出一个值的指令和一条使用该值的指令之间,流水线必须暂停一个周期。
处理r e t : 流水线必须暂停直到 r 过 指令到达写回阶段。
预测错误的分支:在分支逻辑发现不应该选择分支之前,分支目标处的几条指令巳经进入流水线了。必须取消这些指令,并从跳转指令后面的那条指令开始取指。
异常:当一条指令导致异常,我们想要禁止后面的指令更新程序员可见的状态,并且在异常指令到达写回阶段时,停止执行。
我们先浏览每种情况所期望的行为,然后再设计处理这些情况的控制逻辑。
1 特殊控制情况所期望的处理
在 4. 5. 5 节中, 我们已 经描述了对加载/使用冒险所期望的流水线操作, 如图 4-5 4 所示。只有 mr mo v q 和 p o p q 指令会从内存中读数据。当这两条指令中的任一条处千执行阶 段,并且需要该目的寄存器的指令正处在译码阶段时,我们要将第二条指令阻塞在译码阶 段, 并在下一个周期往执行阶段中插入一个气泡。此后 , 转发逻辑 会解决这个数据冒险。可以将流水线寄存器 D 保持为固定状 态, 从而将一个指令阻 塞在译码阶段。这样做还可以保证流水线 寄存器 F 保持为固定状态, 由此下一条指令会被再取一次。总之, 实现这个流 水线流需要发现冒险的 清况 , 保持流水线 寄存器 F 和 D 固定不变,并 且在执 行阶段中插入气泡。
对r e 七指令的处理,我们已经 在 4. 5. 5 节中描述 了所需的流水线操作 。流水线要停顿
3 个时钟周期, 直到r e 七指令经过访存阶段, 读出返回地址。通过图 4-55 中下面程序的处理的简化流水线图,说明了这种情况:
OxOOO: | irmovq stack,%rsp | # | Initialize stack pointer |
---|---|---|---|
OxOOa: | call proc | # | Procedure call |
Ox013: | irmovq $10,%rdx | # | Return point |
Ox01d: | halt |
Ox020: . pos Ox20
Ox020: proc: # proc:
Ox020: ret # Return immediately Ox021: rrmovq %rdx,%rbx # Not executed Ox030: . pos Ox30
Ox030: stack: # stack: Stack pointer
图4-62 是示例程序中 r e t 指令的实际处理过程。在此可以看到, 没有办法在流水线的取指阶段中插入气泡。每个周期 , 取指阶段从指令内存中读出一条指令。看看 4. 5. 7 节中实现 P C 预测逻辑的 HCL 代码,我 们可以 看到, 对r e 七指令来说, PC 的 新值被预测成valP, 也就是下一条指令的地址。在我们的示 例程序中, 这个地址会是 Ox 021 , 即 r e t 后面r r mo v q 指令的地址。对这个例子来说, 这种预测是不对的, 即使对大部分情况来说, 也是不对的, 但是在设计中, 我们并 不试图正确预测返 回地址。取指阶段会暂停 3 个时钟
周期, 导致取出r rmo v q 指令, 但是在译码阶段就被替换成了气泡。这个过程在图 4- 6 2 中的表示为 , 3 个取指用箭头指 向下面的气 泡,气 泡会经过剩下的流水线阶段。最后, 在周期7 取出 i r mo v q 指令。比较图 4- 62 和图 4- 55 , 可以看到,我们的实现达到了期望的效果, 只不过连续 3 个周期取出了不正确的指令。
# prog6
OxOOO: irmovq St ack , %r s p OxOOa: call proc
Ox020: ret
2 3 4 5 6 7 8 9 10 11
MIW #
F I D I E I M I W
F ( D J E I M I W
Ox021: rrmovq %r dx , 1r儿 bubble
b x # Not executed
F
曰E I M I w
Ox021: rrmovq %r d x , %r b x # Not executed bubble
Ox021 : r r movq %r d x , %r b x # Not e xecut ed
F
D I E I M I w
F
图 4-62 r e t 指令的详细处 理过程。取指阶段反复取出r e t 指令后面的r r movq 指 令 ,但 是 流 水 线 控 制逻辑在译码阶段中插入气泡,而 不 是 让 r r movq 指 令 继 续 下 去 。 由 此 得 到 的 行 为与图 4-55 所示的等价
当分支预测错误发 生时, 我们已 经在 4. 5. 5 节中描述了所需的流水线操作, 并用图 4-
56 进行了说明 。当跳转指令到达执行 阶段时就可以检测到预测错误。然后在下一个时钟周期, 控制逻辑就 会在译码和执行段插入气泡 , 取消两条不正 确的已取指令。在同一个时钟周期 , 流水线将正确的指令 读取到取指阶段。
对于导致异常的指令, 我们必须使 流水线化的实现符合期望的 ISA 行为, 也就是在前面所有的指令结束 前, 后面的指令不能影响程 序的状态。一些因素会使得想达到这些 效果比较麻烦: 1 ) 异常在程序执行 的两个不同阶段(取指和访存)被发现的, 2 ) 程序状态在三个不同阶段(执行、访存和写回)被更新。
在我们的 阶段设计中, 每个流水线寄存器中会包含一个状态码 s t a t , 随着每条指令经过流水 线阶段, 它会记录指令的 状态。当异常发生时, 我们将这个信息作为指令状态的一部分记录下来, 并且继续取指、译码和执行指令, 就好像什么都没有出错似的。当异常指令到达访存阶段时,我们会采取措施防止后面的指令修改程序员可见的状态: 1 ) 禁止执行阶段中的指令设置条件码, 2 ) 向内存阶段中插入气泡, 以禁止向数据内存中写入, 3 ) 当写回阶段中有异常指令时,暂 停 写回阶段, 因而暂停了流水线。
图 4- 63 中的流水线图说明了我们的流水线控制如何处理导致异常的指令后面跟着一条会改变条件码的指令的 情况。在周期 6 , p u s h q 指令到达访存 阶段, 产生一个内存错误。在同一个周期, 执行阶段中的 addq 指令产生新的条件码的 值。当访存或者写回阶段中有异常指令时(通过检查信号m_ s t a t 和 W_ s t a t , 然后将信号 s e t _ c c 设置为 0) , 禁止设置条件码。在图 4- 63 的例子中, 我们还可以 看到既 向访存阶段插入了气泡, 也在写 回阶段暂停了异常指令一- p u s hq 指令在写回阶段保持暂停, 后面的指令都没有通过 执行阶段。
对状态信号流水 线化, 控制条件码的设置, 以及控制流水线阶段一 将这些结合起
来,我们实现了对异常的期望的行为:异常指令之前的指令都完成了,而后面的指令对程
序员可见的状态都没有影响。
Mlwlwlwl ..·@
图 4- 63 处理非法内 存引用异常。在周期 6 , pu shq 指令的非法内存引用导 致禁止更新 条件码。流水线开始往访存阶段插入气 泡, 并在写回 阶段暂停 异常指令
2 发现特殊控 制条件
图 4-64 总结了需要特殊流水线控制的条件。它给出的表达式 描述了在哪些条件下会出现这三种特殊情况 。一些简单的组合逻辑块实现了 这些表达式 , 为了在时钟上升开始下一个周期时控制流水线寄存器的活动, 这些块 必须在时钟周期 结束之前产生出结果。在- 个时钟周期内,流 水线寄存器 D、E 和 M 分别保持着处千译码、执行和访存阶段中的指令的 状态。在到达时钟 周期末尾时 , 信号 d _ sr c A 和 d _ sr c B 会被设置为译码阶段中指令的源操作数的寄存器 ID。当 r e t 指令通过流水线时, 要想发现它,只 要检查译码、执行和访 存阶段中指令的指令码。发现加载/使用冒险要检查执行阶段中的指令类型( mr mo v q 或 p op q ) ’ 并把它的目的寄存器与译码阶段中 指令的源寄存器相比较。当跳转指令在执行阶段时, 流水线控制逻辑应该能发 现预测错误 的分支, 这样当指令进入访存阶段时, 它就能设 置从错 误预测中恢复所需要的条件。当跳转指 令处于执行阶段时, 信号 e _ Cn d 指明是否要选择分支。通过 检查访存和写回阶段中的指令状态值, 就能发现异常指令。对于访存阶段,我 们使用在这个阶段中计算出来的信号 m _ s t a t , 而不是使用流水线寄存器的 M s t a t 。这个内部信号包含着可能的数 据内存地址错误。
条件 触发条件
处理 r e t
加载/使用冒险预测错误的分支异常
IRETE {D_icode, E_ic ode, M _icode}
E_icodeE { IMRM OVL, IPOPL} & & E_dstME { d_sr cA, d_srcB }
E_icode= IJXX & & ! e_Cnd
m_statE { SADR,SINS,SHLT} I IW_statE { SADR, SINS,SHLT}
图 4- 64 流水线控制逻辑的检查条件。四种不同的条件要求改变流水线, 暂停流水线或者取 消已经部分执行的指 令
流水线控制机制
图 4-65 是一些低级机制, 它们使得流水线控制逻辑能将指令阻塞在流水线寄存器中,
或是往流 水线中插入一个气 泡。这些机制包括对 4. 2. 5 节中描述的基本时钟寄存器的小扩展。假 设每个流水线 寄存器有两个控制输入:暂 停( stall) 和气泡C bubble ) 。这些信号的设
置决定 了当时钟上升时该 如何更新流水线寄存器。在正常操作下(图4-65a ) , 这两个输入都设为 o, 使得寄存器加载它的输入作为新的状 态。当暂停信号设为 1 时(图4-65 b) , 禁止
更新状态。相反,寄存器会保持它以前的状态。这使得它可以将指令阻塞在某个流水线阶 段中。 当气泡信号设 置为 1 时(图4-65c) , 寄存器状态 会设置成某个固定 的复位 配置 ( res et
configuration), 得到一个等效于 no p 指令的状态。一个流水线寄存器的复位配置的 O、 1 模式是由流 水线寄存器中字段的集合决定的。例如,要 往流水线寄存器 D 中 插入一个气泡, 我们要将 i c o d e 字段设置为常数值 IN OP( 图 4-26 ) 。要往流水线寄存器 E 中插入一个气泡,我们要 将 i c o d e 字段设为 I NOP, 并将 d s t E、d s t M、sr c A 和 sr cB 字段设为常数
RNONE。 确定复 位配置是 硬件设计师在设计流水线寄存器时的任务之一。在此我们不讨论细节。 我们会将气泡 和暂停信号都设为 1 看成是出错。
状态=x 状态=y
门
一呻 时钟上升沿 -+
工 #
- 正常
状态=x 状态==x
门 输出=x
工
b ) 暂停
状态=x 状态=nop
门句 #
工
b ) 气泡
图 4-65 附加的流水线寄存器操作。a ) 在正常条件下 , 当时钟上升时 , 寄存器的状态和输出 被设置成输入的值; b ) 当运行在暂停模式中时,状 态保持为先前 的值不变; c ) 当运行在气泡模式中时, 会用 nop 操作的状态覆盖 当前状态
图 4-66 中的表给出了各个 流水线寄存器在三种特殊情况下应该采取的行动。对每种情况的处理都是对流水线寄存器正常、暂停和气泡操作的某个组合。在时序方面,流水线 寄存器的暂停和气泡控制信号是由组合逻辑块产生的。当时钟上升时,这些值必须是合法 的,使得当下一个时钟周期开始时,每个流水线寄存器要么加载,要么暂停,要么产生气
泡。有了这个对流水线寄存器设计的小扩展,我们就能用组合逻辑、时钟寄存器和随机访问存储器这样的基本构建块,来实现一个完整的、包括所有控制的流水线。
流水线寄存器 条件 | |||||
---|---|---|---|---|---|
处理r et | F 暂停 | D 气泡 | E 正常 | M 正常 | w 正常 |
加载/使用冒险 | 暂停 | 暂停 | 气泡 | 正常 | 正常 |
预测错误的分支 | 正常 | 气泡 | 气泡 | 正常 | 正常 |
图 4-66 流水线控制逻辑的动作。不同的条件需要改变流水线流,或者会暂停流水线, 或者会取消部分已 执行的指令
4 控制条件的组合
到目前为止,在我们对特殊流水线控制条件的讨论中,假设在任意一个时钟周期内, 最多只能出现一个特殊情况。在设计系统时,一 个 常 见的缺陷是不能处理同时出现多个特殊情况的 情形 。现在来分析这些可能性。我们不需要担心多个程序异常的组合情况, 因为已经很小心地设计了异常处理机制, 它 能 够 考虑流水线中其他指令的情况。图 4-67 画出了导致其他三种特殊控制条件的流水线状态。图中所示的是译码、执行和访存阶段的块。 暗色的方框代表要出现这种条件必须要满足的特别限制。加载/使用冒险要求执行阶段中 的 指令 将一 个值从内存读到寄存器中,同 时 译 码 阶 段 中 的 指 令 要 以 该 寄 存 器 作 为源操作数 。预测 错 误的 分支要求 执 行阶段中的指令是一个跳转指令。对r 琴 来说有三种可能的情况一 指令可以处在译码、执行或访存阶段。当r e t 指令通过流水线时,前 面的流水线阶段都是气泡。
加载/使用 预测错误
ret 1 ret 2 ret 3
五勹巨勹二三丿``
图 4- 67 特殊控制条件的流水线状态。图中标明的两对情况可能同时出现
从这些图中我们可以看出, 大 多 数 控 制 条 件 是 互 斥的。例如,不 可能同时既有加载/ 使用 冒险又有预测错误的分支,因 为加载/使用冒险要求执行 阶段中是加载指令C mr movq 或 p opq )’ 而 预 测 错 误 的 分 支要 求 执 行 阶 段中是一条跳转指令。类似地, 第二个和第三个r e t 组 合 也 不 可 能 与 加 载/使用冒险或预测错误的分支同时出现。只有用箭头标明的两种组合可能同时出现。
组合 A 中执行阶段中有一条不选择分支的跳转指令,而 译 码 阶 段 中 有 一 条 r e t 指令。出 现这种组合要求r e t 位于不选择分支的目标处。流水线控制逻辑应该发现分支预测错误 ,因 此 要 取 消 r e t 指令。
练习题 4. 37 写 一个 Y86-64 汇编 语言程序, 它 能 导致出现组合 A 的情 况, 并判断控制逻辑是否处理正确。
合并组合 A 条件的控制动作(图4-66) , 我们得到以下流水线控制动作(假设气泡或暂
停会覆盖正常的情况):
;!1
也就是说 , 组合情况 A 的处理与预测错误的分支相似,只 不过在取指阶段 是暂停。幸运的是,在下一个周期 , P C 选择逻辑会选择跳转后面那条指令的地址, 而不是预测的程序计数器值, 所以流水线寄存器 F 发生了什么是没有关系的。因此我们得出 结论,流 水线能正确处理这种组合悄况。
组合 B 包括一个加载/使用 冒险, 其中加载指令设置寄存器%r s p , 然后r e t 指令用这个寄存器 作为源操作数 , 因为它必须从栈中弹出返回地址。流水线控制逻辑应该 将r e t 指令阻塞在译码阶段。
练习题 4. 38 写 一个 Y8 6- 64 汇编语 言程 序, 它能导致 出现 组合 B 的 情况 , 如果 流水线运行 正确,以 ha l t 指令 结束。
合并组 合 B 条件的控制动作(图4-66 ) , 我们得到以下流水线控制动作:
流水线寄存器 条件 | |||||
---|---|---|---|---|---|
处理 re t | F 暂停 | D 气泡 | E 正常 | M 正常 | w 正常 |
预测错误的分支 | 暂停 | 暂停 | 气泡 | 正常 | 正常 |
组合 | 暂停 | 气泡+暂停 | 气泡 | 正常 | 正常 |
期望的情况 | 暂停 | 暂停 | 气泡 | 正常 | 正常 |
如果同时触 发两组动作 , 控制逻辑 会试图暂停r e t 指令来避免加载/使用冒险, 同时又会因 为r e t 指令而往译码阶段中插入一个气泡。显然 , 我们不希望流水线同时执行这两组动作。相 反, 我们希望它只采取针对加载/使用冒险的动作。处理 r e t 指令的动作应该推迟一个周期。
这些分析 表明组合 B 需要特殊处理。实际上, PIPE 控制逻辑原来的实现并没有正确处理这种组合情况。即使设计已经通过了许多模拟测试,它还是有细节问题,只有通过刚 才那样的分 析才能发现。当执行一个含有组合 B 的程序时 , 控制逻辑会 将流水线寄存器 D 的气泡和 暂停信号都置为 1。这个例子表明了系统分析的重要性。只运行 正常的程序是很难发现 这个问题的。如果没有发现这个问题, 流水线就不能忠实地实 现 ISA 的行为。
5 控制逻 辑实现
, 图 4-68 是流水线控制逻辑的整体结构。根据来自流水线寄存器和流水线阶段的信号,控制逻辑产生流水线寄存器的暂停和气泡控制信号,同时也决定是否要更新条件码寄存器。我们可
、 以将图 4-64 的发现条件和图4-66 的动作结合起来,产生各个流水线控制信号的HCL 描述。
' 遇到加 载/使用冒险或r e t 指令, 流水线寄存器 F 必须暂停:
bool F_stall “’
# Conditions for a load/use hazard E_icode in { IMRMOVQ, IPOPQ} && E_dstM in { d_srcA, d_srcB} II
# Stalling at fetch while ret passes through pipeline IRET in { D_icode, E_icode, M_icode };
图 4-68 PIPE 流水线控制逻辑 。这个逻辑覆盖了通过 流水线的正常指令流 ,以处理特殊条件, 例如过程返回、预测错误的分支、加载/使用冒险和程序异常
练习题 4. 39 写 出 P IP E 实现中信 号 D_ s t a l l 的 HCL 代码。
遇到预测错误的分支或 r e t 指令 , 流水线寄存器 D 必须 设 置为气泡。不过, 正如前面一节中的分析所示, 当遇到加载/使用冒险和r e t 指令组合时,不 应该插入气泡:
bool D_bubble =
# Mispredicted branch
(E_icode == IJXX && !e_Cnd) I I
# Stalling at fetch while ret passes through pipeline
# but not condition for a load/use hazard
!(E_icode in { IMRMOVQ, IPOPQ} && E_dstM in { d_srcA, d_srcB }) &&
IRET in { D_icode, E_icode, M_icode };
练习题 4. 40 写 出 P I P E 实现中信 号 E_ b ub b l e 的 HCL 代码。
沁 员 练习题 4. 41 写 出 P IP E 实现 中 信号 s e 七— c c 的 HCL 代码。该信号 只 有对 OPq 指令
才出现,应该考虑程序异常的影响。
沁囡 练习题 4. 42 写 出 P I P E 实现中信号 M_ b u b b l e 和 W_ s t a l l 的 HCL 代 码。后 一个信号需要修改图 4-64 中列 出的 异常条件。
现在我们讲完了所有的特殊流水线控制信号的值。在 P IP E 的完整 HCL 代码中,所有其他的流水线控制信号都设为 0。
田 日 测试设计
正如我们看到的,即使是对于一个很简单的微处理器,设计中还是有很多地方会出 现问题。使 用流水线, 处于不 同流水线阶段的指令之间有许多 不 易察 觉的 交互 。我们看到一些设计上的挑战来自 于不 常见的指令(例如弹出值 到栈指针), 或是 不 常见的指令组合(例如不选择分支的跳转指令后面跟 一条r e t 指令)。还看到 异常处理增加了 一类全 新的 可能的流水线行 为。 那么怎样确定我们的设计是正确的呢? 对于硬件制造者来说,这
是主要 关心的 问题 , 因为他 们不能 简 单 地 报 告 一 个 错 误 , 让 用 户通过 Inter net 下栽代码 甘
补丁。 即 使 是 简单的逻辑设计错误都可能有很严 重的后 果, 特 别是 随 着微 处理 器越 来越多地用于对我们的生命和健康至关重要的系统的运行中,例如汽车防抱死制动系统、心 脏起 搏 器以 及 航 空控制 系统 。
简单 地 模 拟 设 计, 运 行 一 些“典型的“ 程序, 不足 以 用 来测试一 个 系统 。 相 反 , 全面的测试需要设计一些方法,系统地产生许多测试尽可能多地使用不同指令和指令组 合。在创 建 Y86-64 处理 器的 过 程 中 , 我 们还设计 了 很 多 测试脚本, 每 个脚 本都产 生 出很多不 同的测试, 运 行 处 理 器模拟 , 并 且比较 得到的寄存 器和 内存值 和我们 YIS 指 令 集模拟 器产 生的 值。以 下是这 些脚本的 简 要 介 绍:
optest: 运行 49 个 不同的 Y86-64 指令 测试, 具 有 不同的 源 和 目 的 寄 存 器。
jtest: 运行64 个不同的 跳转和函数 调 用指令 的 测试,具 有 不同的是否选择 分支的组合。
cmtest: 运行 28 个不同的条件传送指令的测试, 具 有 不同的 控 制组合。
htest: 运行 600 个不同的 数 据 冒险可能性的测试, 具 有 不同的 源 和 目的 的 指 令 的 组合,在 这些指令对之 间有 不同数 量的 na p 指令 。
ctest : 测试 22 个不同的控制组合 , 基 于类似 4. 5. 8 节 中我们做的那样的分析。
etest: 测试 12 种不同的 导致异 常的指令和跟在后面可能改 变程序 员可见状态的指令 组合。这种测试方法的关键思想是我们想要尽量的系统化,生成的测试会创建出不同的可
能导致流水线错误的条件。
m 形式 化地 验证 我们的设计
即使一个设计通过了广泛的测试,我们也不能保证对于所有可能的程序,它都能正 确运行。即使只考虑由短的代码段组成的测试,可以测试的可能的程序的数量也大得难 以想象。 不过 , 形 式化验证 ( fo rmal veri fic ation ) 的新方 法能 够保证 有 工具能 够严格 地 考虑一 个 系统 所有可能的行为 , 并 确定是否有设计错误。
我们能够形式化验证 Y86-64 处理 器较 早 的 一 个版 本[ 13] 。 建 立一 个框 架 , 比 较 流
水线化 的设计 PIPE 和非 流水线化的版 本 SEQ。也 就是 , 它 能 够证 明 对 于任 意 Y86-6 4 程序 , 两 个处理器对程 序 员 可见的状 态有 完全一样的影响。 当 然 , 我 们的验证 器不可能真的运行 所有 可能的程序, 因 为这 样 的 程 序 的 数 量是 无 穷大的。相 反 , 它使 用 了 归纳 法来证明, 表 明 两个处理 器之 间在一 个周期到一个周期的基础上都是 一致的。进行这种分析要求用符号方法( sym bolic met hods ) 来推导硬件, 在 符 号 方 法中, 我 们认为 所 有 的 程序值都是任意的整数, 将 ALU 抽 象成 某种 “ 黑盒子“, 根 据 它的 参数 计算 某个未指定的函数。我们只假设 SEQ 和 PIPE 的 ALU 计算相同的函数 。
用控制逻辑的 HCL 描 述来产 生符号 处理 器模 型的控制逻辑 , 因 此 我们能发现 HCL 代码 中的 问题。能够证 明 SEQ 和 PIPE 是 完全 相 同 的 , 也 不 能保 证 它 们 忠 实地 实 现 了Y86-64 指令 集体 系结 构 。 不过 , 它能 够发现任何 由 于不 正确的 流水线设计导致的错误 , 这是设计错误的主要来源。
在实验 中 , 我们不仅验证 了在 本 章 中考虑的 PIPE 版本, 还 验 证 了 作 为 家庭 作 业的几个变种,其中,我们增加了更多的指令,修改了硬件的能力,或是使用了不同的分支 预测 策略 。有趣的是 , 在 所 有 的 设 计 中, 只发 现 了 一 个错误 , 涉及 家庭 作 业 4. 58 中描 述的 变种 的 答 案中的控制组合 BC在 4. 5. 8 节中讲述的)。这暴露出测试体制中的一个弱点,
导致我们在 ctes t 测试脚本中增加 了附 加的情 况。
形式化 验证仍然处在发展 的早期阶段。工具往往很 难使 用, 而且还不能 验证大规模的设计。我们能 够验证 Y86-64 处理 器的 部分原 因就是 因 为 它们相 对比较简单 。即使如此,也 需要 几周的 时间和 精力, 多次运行 那些 工具, 每次最多 需要 8 个小时的 计算机时间。 这是一个活跃的 研究领域 ,有 些工具成为 可用的 商业版 本, 有些在 In tel、AMD 和IB M 这样的公 司使用。
_ _ 流水线化的 Y86-6 4 处理器的 Ve rilog 实现
正如我们提到过的 , 现代的逻辑设计 包括用硬件描述语 言书 写硬 件设计的 文本表示。 然后 , 可以 通过模拟和各种形式化 验证工具来测试设 计。一旦 对设计有了信心,我们就 可以使 用逻 辑合成( log ic s ynt hesis ) 工具将设计翻译成 实际的 逻辑电路 。
我们用 Ver ilog 硬件描述语 言开发 了 Y86-64 处理 器设 计的模 型。这些设 计将 实现处理器基本构造块的模 块和 直接从 H CL 描述产 生出来的 控制逻辑结合了起来。我们能够合成这些设 计的 一些 , 将逻辑电路描 述下栽到 字段可编 程的 门阵列 CF PG A ) 硬件上,可以在这些处理 器上运行 实际的 Y86-6 4 程序。
5. 9 性能分析
我们可以看到, 所有需要流水线控制逻辑进行特殊处理的条件, 都会导致流水线不能够 实现每个时钟周期发射一条新指令的目标。我们可以通过确定往流水线中插入气泡的频率 ,来衡最这种效率的损失, 因为插入气泡 会导致未使用的流水线周期。一条返回指令会产生三个气泡, 一个加载/使用冒险会产生一个,而 一个预测错误的分支会产生两个。我们可以通过计算 PIP E 执行一条指令所需要的平均时钟周期数的估计值, 来量化这些处罚对整体性能的影响, 这种衡量方法称为 CP I (Cycles Per Instruction, 每指令周期数)。这种 衡量值是流水线平均吞吐量的倒数 , 不过时间单位是时钟周期, 而不是微微秒。这是一个设计体系结构效率的很有用的衡量标准。
如果我们忽略异常带来的性能损失(异常的定义表明它是很少出现的), 另一种思考 CPI 的方法是,假设我们在处理器上运行某个基准程序,并 观察执行阶段的运行。每个周期,执行阶段要么会处理一条指令,然后这条指令继续通过剩下的阶段, 直到完成; 要么会处理一个由丁三种特殊情况之一而插入的气泡。如果这个阶段一共处理了 C,条指令和 G个气泡, 那么处理器总共需要大约 C, 十G个时钟周期来执行 C条指 令。我们说“大约” 是因为忽略了启动指令通过流水线的周期。于是, 可以用如下方法来计算这个基准程序的 CPI :
CPI= C, 十 Cb
C,
=LO+_,,C_
C,
也就是说, CPI 等千 1. 0 加上一个处罚项 Cb / C, , 这个项表明执行一条指令平均要插入多少个气泡。因为只有三种指令类型会导致插入气泡, 我们可以将这个处罚项分解成三个部分:
C PI = 1. 0 + lp + mp +r p
这里, l p Cloa d penalt y , 加载处罚)是当由于加载/使用冒险 造成暂停时插入气泡的平均数, mp ( mis predict ed branch penalt y , 预测错误分支处罚)是当由于预测错误取消指令时 插入气泡的平均数 ,而 r p ( ret ur n penalt y , 返回处罚)是当由于r e t 指令造成暂停时插
人气泡的 平均数。每种处罚都是由该种原因引起的插入气泡的总数( Cb 的 一部分)除以执行指令的总数( C;) 。
为了估计每种处罚,我们需要知道相关指令(加载、条件转移和返回)的出现频率,以 及对每种指 令特殊情况出现的频率。对 CPI 的计算, 我们使用下 面这 组频率(等同于[ 44] 和[ 46] 中 报 告 的 测 量值):
加 载指令( mr mov q 和 p op q ) 占所有执行指令的 25 % 。其中 20 % 会导 致加 载/使用冒险。
条件分支指令占所有执行指令的 20 % 。其中 60% 会选择分支, 而 40 %不选择分支。
返回指令占所有执行指令的 2% 。
因此,我们可以估计每种处罚,它是指令类型频率、条件出现频率和当条件出现时插 入气泡数的 乘积:
原因 | 名称 | 指令频率 | 条件频率 | 气泡 | 乘积 |
---|---|---|---|---|---|
加载/使用 | Ip | o. 25 | 0. 20 | 1 | 0. 05 |
预测错误 | mp | 0. 20 | 0. 40 | 2 | o. 16 |
返回 | rp | o. 02 | 1. 00 | 3 | 0. 06 |
总处罚 | 0. 27 |
三种处罚的总和是 0. 27, 所以得到 CPI 为 l. 27。
我们的 目标是设计一个每个周期发射一条指令的流水线,也 就 是 CPI 为 1. 0。虽 然没有完全 达到目标,但 是 整体 性能巳 经很 不 错 了 。我们还能看到,要 想 进一步降低 CPI , 就应该集中注意力预测错误的分支。它们占 到了 整个处罚 0. 27 中 的 0. 16 , 因为条件转移非常常见,我们的预测策略又经常出错,而每次预测错误都要取消两条指令。
练习题 4. 43 假设我们 使用 了 一种成功 率 可 以达到 65 % 的分支预 测 策 略, 例 如后 向分支选择、前向分支就 不选择 ( BT F NT ) , 如 4. 5. 4 节 中描述的那样。那 么 对 CPI 有什么样的影响呢?假设其他所有频率都不变。
练习题 4. 44 让我们来分析你为 练 习题 4. 4 和练 习题 4. 5 写的 程序中使 用条件数据传送和 条件控制 转移的 相对 性能。 假设用 这些 程 序 计 算 一个非 常 长 的 数 组 的 绝 对值的和, 所以整体 性能 主要是由内循环所需要的周期数决定的。假设跳 转指 令预测 为 选择分支 , 而大约 50 % 的数 组值 为 正。
平均来 说, 这两个 程序的内循环中执行了 多少 条指令?
平均来 说, 这两个程序的内循环中插入了 多少 个气泡?
对这两个 程序来说, 每个数 组元 素平均需要 多少个时钟周 期?
5. 10 未完成的工作
我们已经创建了 PIPE 流水线化的微处理器结构, 设 计 了 控制逻辑块,并 实 现了处理普通流水线流不足以处理的特殊情况的流水线控制逻辑。不过, PIP E 还是缺乏一些实际微处理器设计中所 必需的关键特性。我们会强调其中一些, 并 讨 论 要 增 加 这些特性需要些什么。
多周期指令
Y86- 64 指令集中的所有指令都包括一些简单的操作,例 如数字加法。这些操作可以在执行 阶段中一个周期内处理完。在一个更完整的指令集中, 我们还将实现一些需要更为复杂操作的指令,例 如 , 整数乘法和除法,以 及 浮点运算。在一个像 PIPE 这样性能中等
的处理器中, 这些操作的典型执行时间从浮点 加法的 3 或 4 个周期到整数除法的 64 个周期。为了实现这些指令,我们既需要额外的硬件来执行这些计算,还需要一种机制来协调这些指令的处理与流水线其他部分之间的关系。
实现多周期指令的一种简单方法就是简单地扩展执行阶段逻辑的功能,添加一些整数和浮点算术运算单元。一条指令在执行阶段中逗留它所需要的多个时钟周期,会导致取指和译码阶段暂停。这种方法实现起来很简单,但是得到的性能并不是太好。
通过采用独立千主流水线的特殊硬件功能单元来处理较为复杂的操作,可以得到更好 的性能。通常,有一个功能单元来执行整数乘法和除法,还有一个来执行浮点操作。当一条指令进入译码阶段时,它可以被发射到特殊单元。在这个特殊单元执行该操作时,流水 线会继续处理其他指令。通常,浮点单元本身也是流水线化的,因此多条指令可以在主流 水线和各个单元中并发执行。
不同单元的操作必须同步,以避免出错。比如说,如果在不同单元执行的各个指令之 间有数据相关,控制逻辑可能需要暂停系统的某个部分,直到由系统其他某个部分处理的 操作的结果完成。经常使用各种形式的转发,将结果从系统的一个部分传递到其他部分, 这和前面 PIPE 各个阶段之间的转发一样。虽然与 PIPE 相比,整 个设计变得更为复 杂,但还是可以使用暂停、转发以及流水线 控制等同样的技 术来使 整体行 为与顺序的 ISA 模型相匹配。
与存储系统的接口
在对 PIPE 的描 述中,我 们假设取指单元和数据内存都可以 在一个时钟周期内读或是写内存中任意的位置。我们还忽略了由自我修改代码造成的可能冒险,在自我修改代码中,一条指令对一个存储区域进行写,而后面又从这个区域中读取指令。进一步说,我们 是以存储器位置的虚拟地址来引用它们的,这要求在执行实际的读或写操作之前,要将虚 拟地址翻译成物理地址。显然,要在一个时钟周期内完成所有这些处理是不现实的。更糟 糕的是,要访问的存储器的值可能位于磁盘上,这会需要上百万个时钟周期才能把数据读 入到处理器内存中。
正如将在 第 6 章和第 9 章中讲述的那样, 处理器的存储系统是由多种硬件存储器和管理虚拟内存的操作系统软件共同组成的。存储系统被组织成一个层次结构,较快但是较小 的存储器保持着存储器的一个子集,而较慢但是较大的存储器作为它的后备。最靠近处理 器的一层是高速 缓存( cache ) 存储器,它 提供对最常使用的存储器位置的快速访问。一个典型的处理器有两个第一层高速缓存一-个用于读指令,一个用于读和写数据。另一种 类型的高速缓存存储器, 称为翻译后 备缓冲 器( T ra ns la tion Look-aside Buffer, TLB), 它提 供了从虚拟地址 到物理 地址的快速翻译。将 T LB 和高速缓存结合起来使用, 在大多数时候, 确实可能在 一个时钟周期内读指令并读或是写数据。因此,我 们的处理器对访问存储器的简化看法实际上是很合理的。
虽然高速缓存中保存有最常引用的存储器位置,但是有时候还会出现高速缓存不命中(miss), 也就是有些引用的位置不在高速缓存中。在最好的情况中,可以从较高层的高速缓存或处理器的主存中找到不命中的数据, 这需要 3 ~ 20 个时钟周期。同 时,流水线会简单地暂停,将指令保持在取指或访存阶段,直到高速缓存能够执行读或写操作。至千流水 线设计,通过添加更多的暂停条件到流水线控制逻辑,就能实现这个功能。高速缓存不命 中以及随之而来的与流水线的同步都完全是由硬件来处理的,这样能使所需的时间尽可能 地缩短到 很少数量的时钟周期。
在有些情况中,被引用的存储器位置实际上是存储在磁盘存储器上的。此时,硬件会 产生一个缺页 ( page fault ) 异常信号。同其他异常一样,这 个 异 常 会 导 致处理器调用操作系统的异常处理程序代码。然后这段代码会发起一个从磁盘到主存的传送操作。一旦完成, 操作系统会返回到原来的程序,而导致缺页的指令会被重新执行。这次,存储器引用将成 功,虽然可能会导致高速缓存不命中。让硬件调用操作系统例程,然 后 操 作 系统例程又会 将控制返回给硬件,这就使得硬件和系统软件在处理缺页时能协同工作。因为访问磁盘需 要数百万 个时钟周期, OS 缺页中断处理程序执行的处理所需的几百个时钟周期对性能的影响可以 忽略不计。
从处理器的角度来看,将用暂停来处理短时间的高速缓存不命中和用异常处理来处理长时间的缺页结合起来,能够顾及到存储器访问时由千存储器层次结构引起的所有不可预测性。
四 当前 的微处理器设计
一个五阶段流水线, 例如 已 经讲过的 PIPE 处理 器 , 代表 了 20 世 纪 80 年代 中期的处理 器 设 计 水 平。 Berkeley 的 Patterson 研 究组开发的 RISC 处理 器 原型 是 笫 一 个
SPARC 处理 器 的 基 础, 它 是 S un Microsystems 在 198 7 年 开发 的。Stanford 的 Hen
nessy 研 究组开发 的处理 器由 MIPS T echnologies ( 一个由 H enness y 成立的 公 司)在 19 86 年商业 化了 。 这 两种 处理 器都 使 用 的 是 五阶段 流水线。Int el 的 i48 6 处理 器 用的也是五阶段 流水线, 只 不过阶段之间的职责划 分不 太一样, 它有 两 个译码 阶段 和一个合并的执 行/访存阶段[ 27] 。
这些 流水线化的设计的吞吐量都限制在最多一个时钟周期一条指令。4. 5. 9 小 节 中描述的 CP I (Cycles Per Instruction, 每指令周期)测量值不可能 小于 1. 0。不同的阶段一次只能处 理一条指令。较新的处理器 支持超标量( s uperscalar ) 操作, 意味 着它们通过并行地取 指、译码和执行多条 指令, 可以 实现小于 1. 0 的 CPI。当 超标量处理 器 已经广 泛使用时 , 性能测量标准已经从 CPI 转化成了 它的 倒数- 每周期执行指令的平均数 , 即
IPC。· 对超标量处理 器 未说, IPC 可以 大 于 1. 0 。 最先进的设计使 用 了 一种称 为 乱 序
(out-of-order ) 执行的技术未并行地执行多 条 指令,执 行 的顺序也可能 完全 不同 于它们在程序 中出现的 顺序,但 是 保 留了顺 序 ISA 模型蕴含的整体行为 。 作 为对程序优化的讨论的一部 分, 我们将会在笫 5 章中讨 论这种形式的 执行。
不过,流水线化的处理器并不只有传统的用途。现在出售的大部分处理器都用在嵌 入式系统中,控制着汽车运行、消费产品,以及其他一些系统用户不能直接看到处理器 的设备 。在这些应 用 中, 与性能较 高的模型相比 , 流水线化的处理 器的 简 单性(比如说像我们 在本章中讨论的这样)会降低成本和功耗需求。
最近 ,随 着多核 处理器受到 追捧,有 些人声 称通过在一个芯片 上集成许多 简 单的处理器,比 使 用 少量 更复杂的处理 器能荻得 更 多 的整体计算能力。 这种策略 有时被 称为“多核 ” 处理器[ 10] 。
6 小结
我们已经看到 , 指令集体系结构,即 ISA , 在处理器行为(就指令集合及其编码而言)和如何实现处理器之 间提供了 一层抽象。ISA 提供了程序执行的一种顺序说明 , 也就是一条指令执行完了 , 下一条指令才会开始。
从 IA32 指令开始,大大简化数据类 型、地址模式和指令编码, 我们定义了 Y86-64 指令集。得到的
ISA 既有 RISC 指令集的属性 , 也有 CISC 指令集的属性。然后,将 不同指令组织 放到五 个阶段中处理, 在此,根据被执行的指令的 不同, 每个阶段中的操 作也不相同。据此, 我们构造 了 SEQ 处理器, 其中每个时钟周期执行一条指令,它会通过所有五个阶段。
流水线化通过让不同 的阶段并行操作, 改进了系统的吞吐扯性能。在任意一 个给定的时刻,多条指令被不同的阶段处理。在引入这种并 行性的过程中 , 我们必须非常小心,以 提供与程序的 顺序执行相同的程序级行 为。通过重新调 整 SEQ 各个部分的顺序 ,引 入流水线, 我们得到 SEQ+ , 接着添加流水线寄存器, 创建出 PIPE一流水 线。然后 , 添加了转发 逻辑 , 加速了将结果从 一条指令发送到 另一条指令, 从而提高了流水线的性能。有几种特殊情况需要额外的流水线控制逻辑来暂停或取消一些流水线阶段。
我们的设 计中包括了一些基本的异常处理机制,在此 , 保证只有到异常指令之前的指令会影响程序员可见的状态。实现完整的异常处理远比此更具挑战性。在采用了更深流水线和更多并行性的系统中, 要想正确处理异常就更加复杂了。
在本章中,我们学习了有关处理楛设计的几个重要经验:
管理 复杂性是 首要问 题。想要优 化使用 硬件资 源, 在最小的成本下获得最大的性能。为了实现这个目的, 我们创建了一个非常简单而一 致的框架, 来处理所有不同的指令类型。有了这个框架, 就能够在处理不同指令类型的逻辑中共享硬件单元。
- 我们 不需要 直接实现 IS A。 ISA 的直接实现意味着一个顺序的设 计。为了获得更高的性能,我们想运用硬件能力以同时执行许多操作,这就导致要使用流水线化的设计。通过仔细的设计和分析, 我们 能够处理各种 流水线 冒险, 因此运行一个程序的整体效果, 同 用 ISA 模型获得的效果完全一致。
- 硬件设计人 员必须非常谨慎 小心。 一旦芯片被制造出来, 就几乎不可能改正 任何错误 了。一开始就使设计正确是非常重要的。这就意味着要仔细地分析各种指令类型和组合,甚至千那些看上去没有 意义的情况 , 例如弹出值到栈指针。必须用系统的模拟测试 程序彻底 地测试设计。在开发 PIPE 的控制逻辑中, 我们的设计有个细微的错误 ,只 有通过对控制组合的仔 细而系统的分析才能发现 。
Y86- 64 处理器的 HCL 描 述
本 章 已 经介 绍 几 个 简 单 的 逻 辑 设 计 , 以 及 Y86-64 处 理 器 SEQ 和 PIP E 的 控 制 逻样的 部 分 HCL 代 码 。 我 们 提 供 了 HCL 语 言 的 文 档 和 这 两 个 处 理 器 的 控 制 逻 辑 的 完整:
HCL 描 述 。 这 些描 述 每 个都 只 需要 5 7 页 HCL 代 码 , 完整 地研 究 它们是很 值得 的。
Y86-64 模 拟器 #
本章的实验资料包括 SEQ 和 PIPE 处理器的模拟器。每个模 拟器都有两个版本 :
GUI(图形用户界面)版本在图形窗口中显示内 存、程序代码以及处理器状态。它提供了一种方式简便地查看指令如何通过处理器。控制面板还允许你交互式地重启动、单步或运行模拟器。
文本版本运行的是 相同的模拟器, 但是它显示信息 的唯 一方式是打印到终端上。对 调试来讲 ,这个版本不是很有用,但是它允许处理器的自动测试。
这些模拟器的 控制逻辑是通过将逻 辑块的 HCL 声明翻译 成 C 代码产生的。然后, 编译这些代码井与模拟代码的其他部分进行链接。这样的结合使得你可以用这些模拟器测试原始设计的各种变种。提供 的测试脚本,它们全面地测试各种指令以及各种冒险的可能性。
参考文献说明
对千那些有兴 趣更多地学习逻辑设 计的人来说 , Katz 的逻辑设计教科书[ 58] 是标准的入门 教材,它强调了硬件描述语言的 使用。Hennessy 和 Patterson 的计算机体系结构教科书[ 46] 覆盖了处 理器设计的广泛内 容,包 括这里 讲述的简单流水 线, 还有并行执行更多指令的更高级的处理器。Shriver 和 Smith [ 101] 详 细介绍 了 AMD 制造的与 Intel 兼 容的 IA32 处理器。
家庭作业 #
- 4. 45 在 3 . 4. 2 节中, x8 6-64 p u s hq 指令被描述成要减少栈指针, 然后将寄存器存储在栈指针的 位置。因此, 如果我们 有一条指令形如对于某个寄存器 R EG , pushq REG, 它等价于下面的代码序列:
subq $8,%rsp movq REG, (%rsp)
Decrement stack pointer Store REG on stack
借助于练习题 4. 7 中所做的分析, 这段代 码序列 正确地描述了指令 p u s hq %r s p 的 行为吗? 请解释。
你该如何改写这段 代码序列, 使得它能够像对 REG 是其他寄存器时 一样, 正确地描述 REG
是% r s p 的情况?
- 4. 46 在 3. 4. 2 节中, x86-64 p op q 指令被描述为将来自 栈顶的结果复制到目的寄存 ff ’ 然后 将栈指针减少。因此,如果我们有一 条指令形 如 p o pq REG, 它等价于下面的代码序列:
movq (%rsp), REG addq $8,%rsp
Read REG from stack Increment stack pointer
借助于练习题 4. B 中 所做的分析, 这段 代码序列 正确地描述了指令 p o p q %r s p 的 行 为吗? 请解释。
你该如何改写这段 代码序列 , 使得它能够像对 REG 是其他寄存器时一样, 正确地描述 REG
是%r s p 的 情况?
·: 4. 47 你的作业是写一 个执行冒泡排序的 Y86-64 程序。下 面这个 C 函数用数组引 用实现冒泡排序,供你参考:
I* Bubble sort: Array version *I
void bubble_a(long *data, long count) {
long i, last;
for (last = count-1; last > 0; last–) {
for (i = O; i < last; i++)
if (data[i +1] < data [i)) {
I* Swap adjacent elements *I long t = data [i +1]; data[i+1] = data[i];
10 data[i) = t;
•• 4. 48
12
13 }
书写并 测试一个 C 版本,它用 指针引用数组元素, 而不是用 数组索引。
书写并测试一 个由这个函数和测试代码组成 的 Y86-64 程序。你 会发现模仿编译你的 C 代码产生的 x86- 64 代码来做实现会很有帮助。虽然指针比较通常是 用无符号算术运算来实现的, 但是在这个练习中,你可以使用有符号算术运算。
修改对家庭作业 4. 47 所写的代码 ,实 现冒泡排序函数的 测试和交换( 6 ~ 11 行),要求不使用跳转, 且最多使用 3 次条件传送 。
修改对家庭作业 4. 47 所写的代码 ,实 现冒泡排序函数的测试 和交换 ( 6~ 11 行), 要求不使用跳转,且只使用 1 次条件传送 。
在 3. 6. 8 节中, 我们看到实 现 s wi t c h 的一 种常见方法是创建一组代码块 ,再 用跳转表对这些块进行索引。考虑图 4-69 中给出的函数 s wi t c h v 的 C 代码, 以及相应的 测试代码 。
用跳转表以 Y86-64 实现 s wi t c h v 。虽 然 Y86-64 指令集不包含间 接跳转指令, 但是, 你可以通过把计算好的 地址入栈 ,再 执行 r e t 指令来获得同 样的效果。实现类 似于 C 语言所示的测试代码, 证明你的 s wi t c hv 实现可以处理触发 d e f a ul t 的情况以 及两个显式处 理的情况。
#include <s t d i o . h>
I* Example use of switch statement *I
long switchv(long idx) { long result = 0; switch(idx) {
case 0:
result= Oxaaa; break;
case 2:
case 5:
result= Oxbbb; break;
case 3:
result= Oxccc; break;
default:
result= Oxddd;
}
return result;
I* Testing Code *I
#define CNT 8
#define MINVAL -1
int main() {
long vals[CNT]; long i;
for (i = O; i < CNT; i++) {
vals[i] = switchv(i + MINVAL);
printf(“idx = %ld, val= Ox%lx\n”, i + MINVAL, vals[i]);
}
return O;
}
图 4-69 Swi t c h 语句可以翻译成 Y86-64 代码。这要求实现一个跳转表
4. 51 练习题 4. 3 介绍了 i a d dq 指令,即将 立即数与 寄存器相加。描述实 现该指令所执行的计算。参考
ir mo vq 和 OPq 指令的计算(图4-18) 。
•• 4. 52 文件 s e q - f u l l. hc l 包含 S EQ 的 HCL 描述,并 将常数 I I ADDQ声明为十六进 制值 c, 也就是 i ad d q 的指 令代码。修改实现 i a dd q 指令的控制逻辑块的 HCL 描述, 就像练习题 4. 3 和家庭作业
4. 51 中描述的 那样。可以参考实验资料获得 如何为你的解答生成模拟器以及如何测试模拟器的指导。
*/ 4. 53 假设要创建一个较低成本的 、基于我们为 P IP E - 设计的结构(图4-41) 的流水线化的处 理器,不使用旁路技术。这个设计用暂停来处理所有的数据相关,直到产生所需值的指令已经通过了写回 阶段。
文件 p i p e - s t a ll . h c l 包含一个对 P IP E 的 HCL 代码的修改版,其 中禁止了旁路逻辑。也就是, 信号 e —v a l A 和 e —v a l B 只是简单地声明 如下 :
## DO NOT MODIFY THE FOLLOWING CODE.
## No f or 口 ar di ng . valA is either valP or value fromr egi s t er file word d_valA = [
D_icode in { ICALL, IJXX} : D_valP; # Use incremented PC
1 : d_rvalA; # Use value read from register file
];
## No forwarding. valB is value from register file word d_valB = d_rvalB;
.. 4. 54
*** 4. 55
*** 4. 56
拿** 4. 57
修改文件结尾处的流水线控制逻辑 , 使之能正确处理 所有可能的控制和数据冒险。作为设计工作的一部分, 你应该分析各种 控制情况的组合, 就像我们 在 PIP E 的流水线控制逻辑设计中做的那样。你会发现有许多不同的组合,因为有更多的情况需要流水线暂停。要确保你的控制逻辑 能正确处理每种组合情况。可以参考实验资料指导你如何为解答生成模拟器以及如何测试模拟 器的。
文件 pi pe - fu ll . hc l 包含一份 P IP E 的 HCL 描述, 以及常数值 I I ADDQ的声明。修改该文件以 实现指令 i a dd q , 就像练习题 4. 3 和家庭作业 4. 51 中描述的那 样。可以 参考实验资料获得如何为你的解答生成模拟器以及如何测试模拟器的指导。
文件 pi pe - nt . hc l 包含一份 P IP E 的 HCL 描述, 并将常数 J _ YES 声明为值 0 , 即无条件转移指令
的功能码。修改分支预测逻辑 ,使 之对条件转移预测为不选择分支 , 而对无条件转移 和 c a l l 预测为选择分支。你需 要设计一种方法来得到跳转目标 地址 v a l e , 并送到流水线寄存器 M , 以便从错误的分支预测中恢复。可以参考实验资料获得如何为你的解答生成模拟器以及如何测试模拟器的 指导。
文件 pi pe -b t fn t . hc l 包含一份 P IP E 的 HCL 描述, 并将常数 J _ YES 声明 为值 o, 即无条件转移
指令的功能码 。修改分支预测逻 辑, 使得当 v a l C< va l P 时(后向分支),就 预测条件转移为选择分支, 当 va l e 娑va l P 时(前向分支), 就预测为不选择分支。(由于 Y86-64 不支持无符号运算,你应该使用有符号比较 来实现这个测试。)并且将无条件转移和 c a l l 预测为选择分支。你需 要设计一种方法来得到 va l C 和 v a l P, 并送到流水线寄存器 M, 以便从错误的分支预测中恢复。可以 参考实验资料获得如何为你的解答生成模拟器以及如何测试模拟器的指导。
在我们的 P IP E 的设计中,只 要一条指令 执行了 l oa d 操作, 从内存中读一个值到寄存 器,并 且下一条指令要用这个寄存器作为源操作数,就会产生一个暂停。如果要在执行阶段中使用这个源操 作数,暂停 是避免冒险的唯一方法。对于第 二条指令将源操作数存储 到内存的情况,例 如 r mmovq 或 p us hq 指令, 是不需要这样的暂停的。考虑下面这段代码示例 :
mrmovq 0(%rcx),%rdx pushq %rdx
nop
popq %rdx
rmmovq %rax,O(%rdx)
# Load 1
# Store 1
# Load 2
# Store 2
在第 1 行和第 2 行 , mr movq 指令从内存读一个值到%r dx , 然后 pu s hq 指令将这个值压入栈中。我们的 P IP E 设计会让 pus hq 指令暂停 , 以避免装载/使用冒险 。不过, 可以 看到, p us hq 指令要到访存阶段才会需 要%r d x 的值。我们 可以再添加一条 旁路通路 , 如图 4-70 所示, 将内存输出
(信号m_va l M) 转发到 流水线寄存器 M 中的 va l A 字段。在下 一个时 钟周期,被传 送的值就能写入内存了。这种技术称为加栽转发 (l oad fo r warding ) 。
注意, 上述代码序列中的第二 个例子(第4 行和第 5 行)不能利用加载转发。p opq 指令加载的值是作为下一条指令地址计算的一部分的,而在执行阶段而非访存阶段就需要这个值了。
- 写出描述发现加载 /使用冒险 条件的逻辑公 式, 类似于图 4-64 所示,除 了 能用加载转发时不会导致暂停以外。
- 文件 pi pe - lf . hc l 包含一个 P IP E 控制逻辑的修改版。它含有信号 e _ v a l A 的 定 义, 用来实现图 4- 70 中标号为 " F w d A” 的块。它还将 流水线控制逻辑中的加载/使用冒险的条件设置为 0’ 因此流水线控制逻辑将不会发现任何形式的加载/使用冒险。修改这个 HCL 描述以实现加载转发。可以参考实验资料获得如何为你的解答生成模拟器以及如何测试模拟器 的指导。
图 4- 70 能够进行加载转发的 执行和访存 阶段。通过 添加一条从内 存输出到流水线寄存器 M 中va l A的源的旁路通路 , 对于这种形 式的加载/使用冒险,我 们可以使用转发 而不必暂停 。这是家庭作业 4. 57 的 主旨
*:4 . 58 我们的流水线化的设 计有点不太现实 ,因 为寄存 器文件有两个写 端口, 然而只有 p op q 指令需要对寄 存器文件同时 进行 两个写操作。因此, 其他指令只使 用一个写端口, 共享这个端口来写 va l E 和va l M。 下面这个图是一个对写回逻辑的 修改版, 其中, 我们 将写回寄存器 lD ( W_ d s t E 和 W_ds t M)
合并成一个信号 w—d s t E, 同时也将写回值 ( W—va l E 和 W_v a l M) 合并成一个信号 w_va l E:
用 HCL 写执行这些合并的逻辑, 如下所示 ;
## Set E port register ID wor d 廿 _ds t E = [
## writing from valM W_dstM != RNONE: W_dstM; 1 : W_dstE;
w_valE w_dslE
];
## Set E port value word w_valE = [
W_dstM != RNONE : W_valM; 1: W_valE;
];
对这些多路复用 器的控制是由 ds t E 确定的一— 当它表明 有某个寄存 器时 , 就选择 端口 E 的值,否则就选择端 口 M 的值。
在模拟模型中 , 我们可以禁 止寄存器端口 M , 如下面这段 H CL 代码所示:
## Disable register port M
## Set M port register ID w or d 廿 _ds t M = RNONE;
## Set M port value word w_valM = O;
接下来的问 题就是要设计处理 popq 的方法。一种方法是用控制逻辑动态地处 理指令 popq
rA, 使之与下 面两条指令序列 有一样的效果:
iaddq $8, %rsp mrmovq -8(%rsp), rA
(关 于指令 i a ddq 的描述, 请参考练习题 4. 3 ) 要注意 两条指令的顺序, 以保证 popq 务r s p 能正确工作。要达到这个 目的,可以 让译码阶段 的逻辑对上面列 出的 popq 指令和 a dd q 指令一视同仁,除了它 会预测下一个 PC 与当前 PC 相等以外。在下一个周期, 再次取出了 po pq 指令, 但是指令代码变成了特殊的 值 I POP2。它会被当作 一条特殊的指令来处理 , 行为与上面列 出的 mr movq 指令一样。
文件 p i pe - l w. hc l 包含上 面讲的 修改 过的 写端口逻辑。它将常数 I POP2 声明为十六进制值E。还包括信号 f _ i c ode 的定义, 它产生流水线寄存器 D 的 i c ode 字段。 可以 修改这个定义,使得当第二次取出 po pq 指令时, 插人指令代码 I POP2。这个 H CL 文件还包含信号 fy c a9?‘i , 也就是标号为 " Se lect PC" 的块(图4-57 ) 在取指阶段 产生的程序计数器的 值。
修改该文 件中的 控制逻辑 , 使之按照我们描述的方式来处理 popq 指令。可以参考实验资料获得如何为你的解答生成模拟器以 及如何测试模 拟器的指导。
., 4, 59 比较三个版本的冒泡 排序的性能(家庭作业 4. 47 、4. 48 和 4. 49 ) 。解释为什么一个版本的性能比其他两个的好 。
练习题 答 案 #
1 手工对指令 编码是非常乏味的 , 但是它将 巩固你对汇编器将汇编代码变成 字节序列的理解。在下面这段 Y86-64 汇编器的输出中 , 每一行都给出了一 个地址 和一个从该 地址开始的 字节序列 :
Oxl OO: I . pos Ox100 # Start code at address Ox100
2 Ox100: 30f30f00000000000000 I irmovq $15,%rbx
3 Ox10a : 2031 rrmovq %rbx, r 儿 cx
4 Ox 10c : I loop:
5 Ox10c: 4013fdffffffffffffff I r mmo v q %rcx, -3(%rbx)
6 Ox116: 6031 I addq %rbx,%rcx
7 Ox118 : 700c01000000000000 I jmp loop
这段编码有些 地方值得 注意 :
- 十进制的 15 ( 第 2 行)的十六 进制表示 为 Ox OOOOOOOOOOOOOOOf 。 以反向顺 序来写就是 Of 00 00 00 00 00 00 00 。
- 十进制 - 3 ( 第 5 行)的十六进制表 示为 Oxf f f ff ff f ff ff ff f d 。 以反向顺 序来写就 f d ff ff f f ff ff ff ff.
- 代码从地址 Ox l OO 开始。第一条指令需要 10 个字节, 而第二条需要 2 个字节。因 此, 循环的目标地址 为 Ox000001 0c 。以反向顺 序来写就是 Oc 01 00 00 00 00 00 00.
2 手工对一个字节序列进行译码 能帮助你理 解处理器面临的 任务 。它必须读入字 节序列 , 并确定要执行什么指令。接下来 ,我 们给出的是用来产生每个字节序列 的汇编代码。在汇编代码的 左边,你 可以看到每条指 令的地址和字节序列。
一些带立即数 和地址偏移 量的操作:
Ox100: 30f3fcffffffffffffff I
Ox10a: 40630008000000000000 I
Ox114: 00 I
包含一 个函数调用的代码 :
Ox200: a06f
Ox202: 800c02000000000000 Ox20b: 00
irmovq $-4,%rbx
rmmovq %rsi,Ox800(%rbx) halt
pushq %rsi call proc halt
Ox20c: I proc:
Ox20c: 30f30a00000000000000 I irmovq $10,%rbx Ox216: 90 I ret
包含非法指令 指示字节 Ox f O 的 代码:
Ox300: 50540700000000000000 I mrmovq 7(%rsp),%rbp
Ox30a: 10 I nop
Ox30b: fO I .byte OxfO # Invalid instruction code
Ox30c: b01f I popq /,rcx
包含一个跳转操作的代码:
Ox400: I loop:
Ox400: 6113 I subq %rcx, %rbx Ox402: 730004000000000000 I je loop
Ox40b: 00 I halt
- pushq 指令中第二个字节非法的代码。
Ox500: 6362 Ox502: aO
code Ox503: fO
specifier byte
xorq %rsi,%rdx
.byte OxaO # pushq instruction
.byte OxfO # Invalid register
4. 3 使用 i a d d q 指令, 我们将 s um 函数重新编写为
# long sum(long *Start, long count)
# start in %rdi, count in %rsi sum :
loop:
test:
xorq %r ax , %r ax andq %rsi,%rsi jmp test
mrmovq (%rdi),%r10 addq %r10,%rax iaddq $8 丛 r di
iaddq $-1,%rsi
jne loop ret
#sum= 0
# Set condition codes
# Get *start
# Add to sum
# start++
# count–
# Stop 吐en 0
4. 4 在x86-64 机器上运行 时, GCC 生成如下r s um 代码:
longr s um(l ong • s tar t , long count) start in¼rdi, count in¼rsi
r sum :
movl $0, %eax testq %rsi, %rsi jle .19
pushq %rbx
movq ( %r d i ) , %rbx
subq $1, %rsi
addq $8, %rdi
call rsum
addq %rbx, %rax
popq %rbx
.L9:
rep; ret
上述代 码很容易改编为 Y86-64 代码:
# long rsum(long *start, long count)
# start in %rdi, count in %rsi rsum:
return:
xorq %rax,%rax andq %rsi,%rsi je return pushq %rbx
mrmovq (%rdi),%rbx irmovq $-1,%r10 addq %r10,%rsi irmovq $8,%r10 addq %r10,%rdi call rsum
addq %rbx,%rax popq %rbx
ret
# Set return value to 0
# Set condition codes
# If count== 0, return 0
# Save callee-saved register
# Get *start
# count–
# start++
# Add *Start to sum
# Restore callee-saved register
5 这道题给了你一个练习写汇编代码的机会。
# long absSum(long *start, long count)
# start in %rdi, count in %rsi
absSum:
irmovq $8,%r8 # Constant 8
irmovq $1,%r9 # Constant 1
xorq %rax,%rax #sum= 0
andq %rsi,%rsi # Set condition codes
jmp test
loop:
mrmovq (%rdi),%r10 # x = *start
xorq %r11, %r11 # Constant 0
12 subq %r10,%r11 # -x
jle pas # Skip if -x <= 0
rrmovq %r11,%r10 # X = -x
pos:
addq %r10,%rax # Add to sum
addq %r8,%rdi # start++
subq %r9,%rsi # count–
test:
jne loop # Stop when 0
ret
4. 6 这道题 给了你一 个练习写带 条件传送 汇编代码的 机会。我们只给出循环的 代码。剩下的部分与练习题 4. 5 的一样。
9 | loop: | ||
---|---|---|---|
10 | mrmovq (%rdi),%r10 | # X = *Start | |
11 | xorq %r11,%r11 | # Constant 0 | |
12 | subq %r10,%r11 | # -x | |
13 | cmovg %r11,%r10 | # If -x > 0 then x = -x | |
14 | addq %r10,%rax | # Add to sum | |
15 | addq %r8,%rdi | # start++ | |
16 | subq %r9,%rsi | # count– | |
17 | test: |
18 jne loop # Stop when 0
[,
4. 7 虽然难以想象这条特殊的指令有什么实际的用处,但 是 在设计一个系统时, 在描述中避免任何歧义是 很 重要的。我们想要为这条指令的行 为确定 一个合理的规则, 并 且 保 证 每个实现都遵循这个规则。
在这个测试中, s ubq 指令将% r s p 的 起 始 值 与压入栈中的值进行了比较。这个减法的结果为 o ,
表明压入的是%r s p 的 旧 值 。
4. 8 更难以想象 为什么会有人想要把值弹出到栈 指针。我们还是应该 确定一个规则,并 且坚 持 它 。这段代码序列将 Oxab cd 压 人栈中, 弹出到%r s p , 然后返回弹出的值。由于结果等于 Oxab cd , 我们可以推断出
popq r% s p 将 栈指针设置为从内存中读出来的那个值。因此, 它等 价 千指令 mrmov q (r% s p ) ,
4. 9 E XCL US IV E-O R 函数要求两个位有相反的值:
bool xor = (!a && b) II (a && !b);
r% s p 。
通常 , 信号 e q 和 xor 是互补的。也就是,一 个 等 于 1’ 另 一 个 就 等于 0。
Eq
由 于 第 一 行 将检 测 出 A 为最小元素的情况,因 此 第 二
b,
行 就 只需 要 确 定 B 还是 C 是最小元素。
a,
4. 12 这个设计只是对从三个输入中找出最小值的简单
改变。 坏
气
word Med3 = [
A<= B && B <= C : B; C <= B && B <= A : B; B <= A && A<= C : A; C <= A && A<= B : A; 1 : C;
图 4-71 练习题 4. 10 的答案
];
4. 13 这些练习使各个阶段的计算更加具体。从目标代码中我们可以看到,指 令 位于地址 Ox 01 6。它由
10 个 字 节 组 成 ,前 两 个字节为 Ox 3 0 和 Oxf 4。后八个字节是 Ox OOO OOOOO OO OOO OBO( 十进制 128) 按
字节反过来的形式。
通用 i r rnov q V, rB ico de , ifun ~ M, [ PC] rA: rB - M1[ PC+ l] vale+- Ms[ PC+ 2] valP 仁 - PC+ l O valE 仁 - o+ valC R[ rB] - valE PC- valP | 具体 | |
---|---|---|
阶段 | ir mo vq $ 1 28, r% s p | |
取指 | icode,ifun- M, [ Ox016] = 3: 0 rA , rB - M, [ Ox017] = f : 4 | |
valC - Ms[ Ox018] = 128 | ||
valP - Ox0 1 6+ 10= Ox020 | ||
译码 | ||
执行 | valE 仁 - o+ 128= 128 | |
访问 | ||
写回 | R[ % r s p ] - valE = 128 | |
更新 PC | PC - valP= Ox020 |
这个指令将寄存器% r s p 设 为 128 , 并将 PC 加 10。
4. 14 我们 可以看到指令位于地址 Ox 02 c , 由两个字节组成,值 分 别 为 Oxb O 和 Ox OO f 。 pus hq 指 令(第 6
行)将 寄 存 器%r s p 设 为了 120 , 并且将 9 存放在了这个内存位置。
valP 仁 - | PC+ 2 | valP 仁 - | Ox02c + 2 = Ox02e | |
---|---|---|---|---|
译码 | valA - valB - | R[ r% sp ] R[ %r s p] | valA - valB - | R[ r% s p] = 120 R[ r毛 s p] = 120 |
执行 | valE - | valB+ 8 | valE - | 120+8= 128 |
访存 | valM - | 队[ valA] | va!M - | Ms[ 120] = 9 |
写回 | R[ %rsp]- valE | R[ %rsp]- 128 | ||
R[ rA] - valM | R 巨 r s p] - 9 | |||
更新 PC | PC 仁 - valP | PC - Ox02e |
该指令将%r a x 设 为 9’ 将 %r s p 设 为 128 , 并将 PC 加 2.
4. 15 沿着图 4- 20 中列 出的步骤, 这里r A 等 于 %r s p , 我们可以看到, 在访存阶段, 指令会将 va l A( 即栈指针的原始值)存放到内存中,与 我 们在 x86-64 中发现的一样。
4. 16 沿着图 4-20 中列出的步骤,这 里r A 等 于 % r s p , 我们可以看到,两 个写回操作都会更新%r s p 。 因
为写 va l M 的 操 作 后 发生,指 令的 最 终 效 果 会 是 将 从内存中读出的值写入% r s p , 就 像 在 x86-6 4 中看到的一样。
4. 17 实现条件传送只需 要对寄存器到寄存器的传送做很小的修改。我们简单地以条件测试的结果作为写回步骤的条件:
4. 18 我们可以看到这条指令位于地址 Ox037, 长度为 9 个字节。第一个字节值为 Ox80, 而后面 8 个字节是
Ox0 000000000000014 按字节反过来的形式,即调用的目标地址。 p::>pq 指令(第7 行)将栈指针设为128。
这条指令的效果 就是将%r s p 设为 1 20 , 将 Ox 0 40( 返回地址)存放到该内存地址 , 并将 PC 设为
Ox 0 41 ( 调用的目标地址)。
4. 19 练习题中所有的 HCL 代码都很简单明了 , 但是试着自己写会帮助你思 考各个指令 , 以及如何处理它们。对于这个问 题, 我们只要 看看 Y8 6- 64 的指令集(图4- 2 ) , 确定哪些有常数字段。
bool need_valC =
icode in { IIRMOVQ, IRMMDVQ, IMRMDVQ, IJXX, !CALL};
4. 20 这段代码类似 千 s r c A 的代码 :
word srcB = [
icode in { IDPQ, IRMMOVQ, IMRMOVQ } : rB;
icode in { IPUSHQ, IPDPQ, ICALL, IRET} : RRSP;
1 : RNONE; # Don’t need register
];
4 . 2 1 这段代码类 似千 d s t E 的代码 :
word dstM = [
icode in { IMRMOVQ, IPOPQ} : rA;
1 : RNDNE; # Don’t write any register
];
4 . 22 像在练习题 4. 16 中发现的那样, 为了将从内 存中读出的值存放到% r s p , 我们 想让通过 M 端口写的优先级 高于通过 E 端口写。
4. 23 这段代码类 似千 a l uA 的代码 :
word aluB = [
icode in { IRMMOVQ, IMRMOVQ, IDPQ, !CALL,
IPUSHQ, I RET, IPOPQ} : valB; icode in { IRRMOVQ, IIRMOVQ} : O;
# Other instructions don’t need ALU
];
4 . 24 实现条件传送令人吃 惊的简单: 当条件不满足时 ,通 过将目的寄存器设置为 RNONE 禁止写寄存器文件。
word dstE = [
icode in { IRRMOVQ} &&: Cnd : rB; icode in { IIRMOVQ, IOPQ} : rB;
icode in { IPUSHQ, IPOPQ, !CALL, IRET} : RRSP;
1 : RNONE; # Don’t write any register
];
4 . 25 这段代码类 似千 me m a d d r 的 代码:
word mem_data = [
# Value from register
icode in { IRMMOVQ, IPUSHQ} : valA;
# Return PC
icode == ICALL : valP;
# Default: Don 飞 口 r i t e anything
]?
4. 26 这段代码类 似于 me m_r e a d 的代码:
bool mem_write = icode in { IRMMOVQ, IPUSHQ, !CALL};
4. 27 计算 S t a t 字段需要从几个阶段收集状 态信息 :
## Determine instruction status word Stat = [
imem_error 11 dmem_error : SADR;
!instr_valid: SINS; icode == !HALT : SHLT;
1 : SAOK·
];
4. 28
4. 29
这个题目非常有趣,它试图在一组划分中找到优化平衡。它提供了大昼的机会来计算许多流水线的吞吐拭和延迟。
对一个两阶段流 水线来说, 最好的划分是块 A、 B 和 C 在第一阶段 , 块 D、 E 和 F 在第二阶段。第一阶段的 延迟 为 170 ps , 所以 整个周期的时长为 1 70 + 20 = 190ps 。因此吞吐量为 5. 26
GIPS, 而延迟为 380ps 。
对一个三阶段 流水线来说 , 应该使块 A 和 B 在第一阶段, 块 C 和 D 在第二阶段, 而块 E 和 F
在第三阶段 。前两个阶段的延迟均 为 ll Ops , 所以 整个周期时长为 130 ps , 而吞吐昼为 7. 69
GIPS 。延迟为 390ps 。
对一个四阶段流水线来说 , 块 A 为第一阶 段, 块 B 和 C 在第二阶段, 块 D 是第三阶段, 而块
E 和 F 在第四 阶段。第二阶段需要 90 ps , 所以整个周期时 长为 ll Ops , 而吞吐最为 9. 09 GIP S。延迟为 440 ps。
最优的设计应该是五阶段 流水线 ,除 了 E 和 F 处千第五阶段以外 , 其他每个块是一个阶段。周期时长为 80 -t- 20 = lOOps, 吞吐批为大约 10. 00 GIPS, 而延迟为 500 ps。变成更多的阶段也不会有帮助了 , 因为不可能使 流水线运行 得比以 l OOps 为一周期 还要快了。
每个阶段的组合逻辑都需 要 300 / k ps , 而流水线 寄存器需要 20 ps。
整个的延迟应该是 300 + 20k ps , 而吞吐量(以 G IPS 为单位)应该是
1 000 1 OOOk
300
k
= 300 + 2 0 k
20
4. 30
当 K 趋近于无穷 大, 吞吐橄变 为 1 000/20=50 GIPS。当然, 这也使得延迟为无穷大。
这个练习题量化了很 深的流水线引起的 收益下降。当我们试 图将逻辑 分割为很 多阶段时, 流水线寄存楛的延迟成 为了一 个制约因素 。
这段代码非常类似于 SEQ 中相应的 代码,除 了我们还不能确定数据内存是 否会为这条指令产生一个错误信号。
# Determine status code for fetched instruction 11ord f_stat• [
imem_error: SADR;
!instr_valid: SINS; f_icode == !HALT : SHLT;
1 : SAOK;
4. 31
];
这段代码只是简单地 给 SEQ 代码中的信号名前加上前缀 " ct_" 和 " o_"。
word d_dstE = [
D_icode in { IRRMOVQ, IIRMOVQ, IOPQ} : D_rB; D_icode in { IPUSHQ, IPOPQ, !CALL, IRET} : RRSP;
- : RNONE; # Don’t write any register
4. 32
];
由于 popq 指令(第4 行)造成的加载/使用冒险, r r movq 指令(第5 行)会暂停一个周期。当它进入译码阶段, popq 指令处于访存阶段 , 使 M_d s t E 和 M_d s t M 都等于 % r s p 。 如果两种情况反过来 , 那么来自 M_va l E 的写回 优先级较高, 导致增加了的栈指针被传 送到 rr movq 指令作为参数。这与练习题 4. 8 中确定的处理 po pq %r s p 的 惯例不 一致。
这个问题让你体验一下处理器设 计中一个很重要的 任务一 为一个新处理器设 计测试程序。通常, 我们的测试程序应该能测试所有的冒险可能性,而且一旦有相关不能被正确处理,就会产生错误 的结果。
对于此例 , 我们可以使用对练习题 4. 32 中所示的 程序稍微修改的版本 :
irmovq $5, %rdx irmovq $0x100,%rsp rmmovq %rdx,0(%rsp) popq %rsp
nop nop
rrmovq %rsp,;r人 ax
4. 34
两个 no p 指令会导致 当r r mo v q 指令在译码 阶段中 时, p o p q 指令处于写 回阶段。如果给予处于写回阶段中的两个转发源错误 的优先级 , 那么寄存器釭a x 会设登成增加了的程序计数器,而不是从内存中读出的值。
这个逻辑只需 要检查 5 个转发源:
word d_valB = [
d_srcB == e_dstE : e_valE; # Forward valE from execute d_srcB == M_dstM: m_valM; # Forward valM from memory d_srcB == M_dstE : M_valE; # Forward valE from memory d_srcB == W_dstM : W_valM; # Forward valM from write back d_srcB == W_dstE : W_valE; # Forward valE from write back
1 : d_rvalB; # Use value read from register file
4. 35
];
这个改变不会处理条件传送不满足条件的情况, 因此将 d s t E 设置为 RNONE。即使条件传送并没有发生,结果 值还是会被转发到下一条指令。
irmovq $0x123,%rax irmovq $0x321,%rdx
xorq %rcx,%rcx cmovne %rax,%rdx addq %rdx,%rdx halt
#cc= 100
# Not transferred
# Should be Ox642
4. 36
这段代码将寄存器% r d x 初始化为 Ox3 2 1 。 条件数据传送没有发生, 所以最后的 a dd q 指令应该 把%r d x 中的值翻倍 , 得到 O x 6 4 2。不过, 在修改过的版本中 , 条件传送源值 Ox l 2 3 被转发到 AL U 的输 入 va l A, 而 v a l B 正 确地 得到 了 操作数值 Ox 3 2 1 。 两 个 输 入 加起来就得到结果 Ox 4 4 4 。
这段代码 完成了对这条指令的状态码的计算。
## Update the status word m_stat = [
dmem_error : SADR;
1 : M_stat;
4. 37
];
设 计下面这个 测试程序来建立控制组合 AC图 4 - 67 ) , 并探测是否出了错:
# Code to generate a combination of not-taken branch and ret irmovq Stack, %rsp
i rmovq rtnp,%rax
pushq ir 儿 ax # Set up return pointer xorq %rax,%rax # Set Z condition code
jne t ar get # No t taken (First part of combination) irmovq $1,%rax # Should execute this
halt
t a 工 ge t : ret
irmovq $2,%rbx halt
# Second part of combination
# Should not execut e this
rtnp:
ir ovq$3,%r dx halt
# Should not execute this
.pos Ox 40 St a c k :
设计这个程序是为了出错(例如如果实际上执行了 r e t 指令)时,程 序会执行一条额外的 江-
movq 指令, 然后停止。因此,流水线中的错误 会导致某个寄存 器更新错 误。这段代码说明实现测试程序需要非常小心。它必须建立起可能的错误条 件, 然后再探 测是否有错误发生。
4. 38 设计下面这个测试 程序用来建 立控制组合 BC图 4-67 ) 。 模拟器会发现流水 线寄存骈的 气泡和暂停
控制信号都设 置成 0 的情况, 因此我们的 测试程序 只需要建立它需 要发现 的组合情况。最大的挑战在千当处理正确时,程序要做正确的事情。
1 # Test instruction that modifies %esp followed by ret
- irmovq mem, %rbx
- mrmovq O(%rbx) , %rsp # Sets %rsp to point to return point
4 | ret | # Returns to return point |
---|---|---|
5 | halt | # |
6 r t npt : 7 | irmovq $5,%rsi halt | # Return point |
a . pos Ox40
- me m: . quad stack # Holds desired stack pointer
- . pos Ox50
- s t a c k : . quad rtnpt # Topof stack: Holds return point
这个 程序使 用了内存中两个初始化了的字。第一 个字( me m) 保存 着第二 个字( s t a c k一 期望的栈指针)的地址。第 二个字保存着 r e t 指令期望的返回点的地址。这 个程序将栈指针加载到
% r s p , 并执行 r e t 指令。
4. 39 从图 4- 66 我们可以 看到,由 千加 载/使用冒险, 流水线寄存 器 D 必须暂停 。
bool D_stall =
# Conditions for a load/use hazard E_icode in { IMRMOVQ, IPOPQ} && E_dstM in { d_srcA, d_srcB };
4. 40 从图 4- 66 中可以看到, 由于加载/使用冒险, 或者由 于分 支预测 错误, 流水线寄存器 E 必须设置成气泡:
bool E_bubble =
# Mispredicted branch
(E_icode == IJXX && !e_Cnd) I I
# Conditions for a load/use hazard E_icode in { IMRMOVQ, IPOPQ} && E_dstM in { d_srcA, d_srcB};
4.41 这个控制 需要检查正 在执行的指令的代码 , 还需要检查流水线中更后 面阶段中的异 常。
## Should the condition codes be updated7 bool set_cc = E_icode == IOPQ &&
# State changes only during normal operation
!m_stat in { SADR, SINS, SHLT} && !W_stat in { SADR, SINS, SHLT };
4. 42 在下一个周期向访存阶段插入气 泡需要检查当前周期 中访存或者写回阶段中是 否有异常。
# Start injecting bubbles as soon as except i on passes through memory stage
bool M_bubble = m_stat in { SADR, SINS, SHLT } I I W_stat in { SADR, SINS, SHLT } ;
对于暂停写回阶段,只用检查这个阶段中的指令的状态。如果当访存阶段中有异常指令时我 们也暂停了, 那么这条指令就不能 进入写回阶段。
bool W_stall = W_stat in { SADR, SINS, SHLT } ;
4. 43 此时, 预测错误的频率是 0. 35, 得到 m p = O. ZO X O. 35X2=0. 14, 而整个 CPI 为 1. 25 。看上 去收获非常小,但是如果实现新的分支预测策略的成本不是很高的话,这样做还是值得的。
44 在这个简化的分析中 , 我们把注意力放在了内 循环上 , 这是估计程序 性能的一种很有用的方法。
只要数组 足够大 , 花在代码 其他部分的时间 可以忽略不 计。
- 使用条件转移的代码的内循环有 9 条指令, 当数组元素是 0 或者为负时 , 这些指令都要执行,
当数组元素为正时 , 要执行其中的 8 条。平均是 8. 5 条。使用条件传送的代码的内 循环有 8 条指令,每次都必须执行。
- 用来实现循环闭合的跳转除了当循环中止时之外,都能预测正确。对于非常长的数组,这个预 测错误对性能的影响可以忽略不计。对于基于跳转的代码,其他唯一可能引起气泡的源取决于 数组元素是否为正的 条件转 移。这会导致两个气泡, 但是只在 50 % 的时间里会出现,所以平均值是 1. 0。在条件传送代码中 ,没有气 泡。
我们的 条件转移代码对于每个元素 平均需 要 8. 5 + 1. 0 = 9. 5 个周期(最好情况要 9 个周期,最差情况要 10 个周期), 而条件传送代码对 千所有的 情况都需要 8. 0 个周期。
我们的 流水线的分 支预测错误处罚只有两 个周期—- 远比 对性能更高的处理器中很 深的流水线造成的处罚要小得多。因此, 使用条件传送对程序性 能的影响不是很 大。