跳转到主要内容

C.1 引言

本书的许多读者可能已经通过其他教材(例如我们编写的基础教材 Computer Organization and Design)或其他课程学习过流水线基础。第 3 章高度依赖这些知识,因此,继续阅读前,应确保自己已经熟悉本附录介绍的概念。阅读第 3 章时,也可以随时回到这里快速复习。

本附录首先介绍流水线基础,包括它对数据通路的影响、冒险,以及流水线性能。本节描述基本的五级 RISC 流水线,后续内容均以此为基础。C.2 节介绍冒险为何影响性能,以及如何处理冒险;C.3 节讨论简单五级流水线的实际实现,重点是控制和冒险处理。

C.4 节讨论流水线与指令集设计各方面的相互作用,其中包括异常以及异常与流水线的关系这一重要主题。如果读者还不熟悉精确与非精确中断,以及异常后的恢复,这些内容会很有帮助,因为它们是理解第 3 章更高级方法的关键。

C.5 节讨论如何扩展五级流水线,以处理执行时间较长的浮点指令。C.6 节通过深流水线处理器 MIPS R4000/4400 的案例,将这些概念联系起来,同时介绍它的八级整数流水线和浮点流水线。MIPS R4000 与单发射嵌入式处理器相似,例如 2010 年推出、曾用于多款智能手机和平板电脑的 ARM Cortex-A5。1

C.7 节引入动态调度,以及用记分牌(scoreboard)实现动态调度的方法。这一内容作为贯穿各层的问题介绍,因为它可以作为第 3 章核心概念的导论;第 3 章着重讨论动态调度方法。C.7 节也为理解第 3 章更复杂的 Tomasulo 算法提供了平缓的入门路径。虽然不先介绍记分牌也能讲解和理解 Tomasulo 算法,但记分牌方法更简单,也更容易掌握。

什么是流水线?

流水线是一种让多条指令重叠执行的实现技术;它利用执行一条指令所需的各项操作之间存在的并行性。如今,流水线是实现高速处理器的关键技术,甚至售价不足一美元的处理器也采用流水线。

流水线类似装配线。汽车装配线有许多工序,每道工序都完成汽车制造的一部分。各道工序并行工作,但处理的是不同的汽车。计算机流水线的每一步完成一条指令的一部分;与装配线一样,不同的步骤并行完成不同指令的不同部分。每一步称为一个流水级或流水段。各级首尾连接,构成流水线:指令从一端进入,逐级前进,再从另一端离开,就像装配线上的汽车。

汽车装配线的吞吐量以每小时生产的汽车数量定义,取决于成品汽车驶离装配线的频率。同样,指令流水线的吞吐量由指令离开流水线的频率决定。由于各级相互连接,就像装配线一样,所有流水级都必须同时做好前进一步的准备。指令沿流水线前进一级所需的时间,称为一个处理器周期。所有级同时推进,因此处理器周期的长度由最慢一级所需的时间决定,正如汽车装配线上最耗时的工序决定了车辆向前推进的间隔。在计算机中,处理器周期几乎总是一个时钟周期。

流水线设计者的目标,是使各级的处理时间均衡,就像装配线设计者努力均衡各道工序的耗时一样。如果各级完全均衡,那么在理想条件下,流水线处理器平均每条指令所需的时间为:

非流水线机器每条指令的时间流水级数 \frac{\text{非流水线机器每条指令的时间}}{\text{流水级数}}

此时,流水线的加速比等于流水级数,就像具有 nn 道工序的装配线,理想情况下可以将汽车产出速度提高到原来的 nn 倍。不过,各级通常不能完全均衡,流水线本身也有额外开销。因此,流水线处理器平均每条指令的时间达不到理论最小值,但可以接近它。

流水线降低了每条指令的平均执行时间。如果原来的处理器执行每条指令需要多个时钟周期,那么流水线会降低 CPI。本附录主要采用这一观察角度。

流水线是一种开发顺序指令流中指令间并行性的实现技术。它有一个显著优势:与某些加速技术不同(见第 4 章),程序员看不到它的存在。

RISC-V 指令集基础

本书使用加载—存储体系结构 RISC-V 说明基本概念。书中介绍的几乎所有思想也适用于其他处理器,但复杂指令的实现可能复杂得多。本节使用 RISC-V 体系结构的核心部分;完整描述见第 1 章。虽然我们使用 RISC-V,但这些概念具有很强的共通性,同样适用于包括 ARM、MIPS 核心体系结构在内的各种 RISC。所有 RISC 体系结构都有几个关键特征:

  • 所有数据运算都针对寄存器中的数据,并且通常修改整个寄存器;每个寄存器为 32 位或 64 位。
  • 只有加载和存储操作影响存储器:前者把数据从存储器送入寄存器,后者把寄存器中的数据送入存储器。通常也提供加载或存储小于整个寄存器宽度的数据的操作,例如字节、16 位或 32 位。
  • 指令格式数量很少,所有指令通常具有相同长度。在 RISC-V 中,寄存器说明符 rs1、rs2 和 rd 总在固定位置,从而简化控制。

这些简单特征极大地简化了流水线实现,这正是这些指令集采用这种设计的原因。第 1 章完整介绍了 RISC-V ISA,这里假定读者已经读过该章。

RISC 指令集的一种简单实现

要理解 RISC 指令集的流水线实现,先要理解非流水线实现。本节给出一种简单方案,每条指令最多需要 5 个时钟周期。我们将把它扩展成流水线版本,大幅降低 CPI。这里的非流水线方案既不是最经济的,也不是性能最高的;选择它,是因为它能自然地过渡到流水线实现。实现指令集需要引入若干不属于体系结构状态的临时寄存器,本节引入它们是为了简化流水线。我们只关注 RISC 体系结构的一个整数子集,包含字加载/存储、分支和整数 ALU 操作。

这个 RISC 子集的每条指令都可以在最多 5 个时钟周期内完成。这 5 个周期如下。

  1. 取指周期(IF): 将程序计数器(PC)送至存储器,取出当前指令。PC 加 4,更新为下一条顺序指令的地址,因为每条指令长 4 字节。
  1. 指令译码/寄存器读取周期(ID): 对指令译码,从寄存器堆读出源寄存器说明符指定的寄存器。在读取寄存器时,检查它们是否相等,以备分支使用。对指令的偏移字段进行符号扩展,留待可能使用。将符号扩展后的偏移加到已递增的 PC 上,计算可能的分支目标地址。

    译码与寄存器读取并行进行,因为 RISC 体系结构的寄存器说明符位置固定。这种技术称为固定字段译码。可能读出了实际上用不到的寄存器,这既无益于性能,也不会损害性能;不过,读取不需要的寄存器会浪费能量,因此重视功耗的设计可能避免这样做。对于加载和 ALU 立即数操作,立即数字段始终在相同位置,因此容易进行符号扩展。若要更完整地实现 RISC-V,就需要计算两个不同的符号扩展值,因为存储指令的立即数字段位于不同位置。

  2. 执行/有效地址计算周期(EX): ALU 对上一周期准备的操作数执行操作,具体功能取决于指令类型:

    • 存储器访问: ALU 将基址寄存器与偏移相加,得到有效地址。
    • 寄存器—寄存器 ALU 指令: ALU 按 ALU 操作码指定的运算,对从寄存器堆读出的值进行操作。
    • 寄存器—立即数 ALU 指令: ALU 按 ALU 操作码指定的运算,对第一个读出的寄存器值和符号扩展后的立即数进行操作。
    • 条件分支: 判断条件是否成立。

    在加载—存储体系结构中,有效地址计算与执行可以合并在同一个周期,因为没有指令需要同时计算数据地址并对该数据进行运算。2

  3. 存储器访问周期(MEM): 对加载指令,存储器使用上一周期算出的有效地址读取数据;对存储指令,则以该有效地址,把从寄存器堆读出的第二个寄存器值写入存储器。

  4. 写回周期(WB): 对寄存器—寄存器 ALU 指令或加载指令,将结果写回寄存器堆。加载结果来自存储系统,ALU 指令结果来自 ALU。

在这一实现中,分支指令需要 3 个周期,存储指令需要 4 个周期,其余指令需要 5 个周期。假定分支频率为 12%、存储频率为 10%,这种典型指令分布对应的总体 CPI 为 4.66。不过,无论是追求最佳性能,还是在给定性能下尽量减少硬件,这个实现都不是最优的。改进这一设计留作练习,下面集中讨论如何把它流水线化。

RISC 处理器的经典五级流水线

只要每个时钟周期启动一条新指令,上一节的执行过程几乎不需要改变,就能实现流水线。这也说明了我们为什么选择这一设计。前面每个时钟周期现在成为一个流水级,也就是流水线中的一个周期,形成图 C.1 所示的执行模式;这也是绘制流水线的典型方式。虽然每条指令需要 5 个周期才能完成,但每个周期硬件都会启动一条新指令,并执行五条不同指令的某一部分。

指令/时钟周期123456789
指令 iiIFIDEXMEMWB
指令 i+1i+1IFIDEXMEMWB
指令 i+2i+2IFIDEXMEMWB
指令 i+3i+3IFIDEXMEMWB
指令 i+4i+4IFIDEXMEMWB
图 C.1 简单 RISC 流水线。每个周期取出另一条指令,开始其五周期执行。若每周期启动一条指令,性能最多可达到非流水线处理器的五倍。各级名称与非流水线实现的各周期相同:IF=取指,ID=译码,EX=执行,MEM=访问存储器,WB=写回。

流水线真有这么简单吗?当然没有。本节和后续各节将解决流水线引入的问题,使这条 RISC 流水线真正可行。

首先,必须确定处理器每个周期发生什么,确保不会在同一周期让同一数据通路资源执行两种不同操作。例如,不能要求一个 ALU 同时计算有效地址并执行减法。因此,必须保证指令在流水线中重叠时不会引发这类冲突。幸运的是,RISC 指令集足够简单,资源使用情况较容易分析。图 C.2 以流水线形式画出简化的 RISC 数据通路。主要功能部件在不同周期使用,因此重叠执行多条指令时,冲突相对较少。这基于三个观察。

首先,指令存储器与数据存储器分离,通常由独立的指令缓存和数据缓存实现(见第 2 章)。如果只有一个存储器,取指与访问数据就会冲突;独立缓存消除了这一冲突。注意,若流水线处理器的时钟周期与非流水线版本相等,存储系统必须提供五倍带宽。这种需求增长,是提高性能的代价之一。

其次,寄存器堆在两个阶段使用:在 ID 读取,在 WB 写入。两种用途不同,所以图中把同一寄存器堆画在两个位置。因此,每个周期需要完成两次读取和一次写入。为处理对同一寄存器的读写,以及稍后就会说明的另一个原因,我们在周期前半段写入,后半段读取。

RISC 数据通路中各条指令随时间重叠
图 C.2 流水线可视为沿时间错开的一组数据通路。图中展示各部分如何重叠,时钟周期 5(CC 5)已进入稳态。寄存器堆在 ID 中提供源操作数,在 WB 中接收结果,所以出现两次。右侧或左侧的实线分别表示在该阶段的一部分时间读取或写入,另一侧为虚线。IM=指令存储器,DM=数据存储器,CC=时钟周期;Reg=寄存器堆,ALU=算术逻辑单元。横轴为时间,纵轴为指令的程序执行顺序。

第三,图 C.2 没有处理 PC。要每个周期启动一条新指令,就必须每周期递增并保存 PC;这必须在 IF 阶段完成,以准备下一条指令。此外,还需要一个加法器,在 ID 阶段计算可能的分支目标地址。另一个问题是,我们需要在 ALU 阶段使用 ALU 判断分支条件。实际上,比较两个寄存器并不需要完整的 ALU,但所需功能已足以使这项工作必须放在这一流水级进行。

除了确保流水线中的不同指令不会同时争用硬件资源,还必须保证处于不同级的指令不会互相干扰。为此,在相邻流水级之间加入流水线寄存器:周期结束时,把某一级的全部结果保存到寄存器中,作为下一周期下一级的输入。图 C.3 给出了包含这些寄存器的流水线。

为简洁起见,许多图省略了流水线寄存器,但流水线正常工作必须有它们。即使是没有流水线的多周期数据通路,也需要类似寄存器,因为只有寄存器中的值能够跨时钟边界保留。在流水线处理器中,这些寄存器还承担一个关键作用:把中间结果从一级送往另一级,而源级和目的级不一定直接相邻。例如,存储指令要写入存储器的寄存器值在 ID 读出,到 MEM 才真正使用,因此必须经过两个流水线寄存器。ALU 指令在 EX 产生结果,到 WB 才真正写回,同样要经过两个流水线寄存器。为便于引用,我们按它们连接的两个流水级命名:IF/ID、ID/EX、EX/MEM 和 MEM/WB。

相邻流水级之间的流水线寄存器
图 C.3 相邻流水级之间加入流水线寄存器。它们避免相邻级中的两条指令相互干扰,也把同一条指令的数据从一级传递到另一级。寄存器的边沿触发特性至关重要,即寄存器值在时钟边沿瞬时改变;否则,一条指令的数据就可能干扰另一条指令的执行。CC=时钟周期,IM=指令存储器,DM=数据存储器,Reg=寄存器堆;横轴为时间。

流水线的基本性能问题

流水线提高了处理器的指令吞吐量,即单位时间完成的指令数,却不缩短单条指令的执行时间。事实上,由于流水线控制开销,每条指令的执行时间通常还会略有增加。吞吐量提高意味着程序运行更快、总执行时间更短,尽管没有任何一条指令本身执行得更快。

单条指令执行时间不缩短这一事实,限制了实际可采用的流水线深度;下一节会看到这一点。除了流水线延迟的限制,还有各级不均衡与流水线额外开销的限制。各级不均衡会降低性能,因为时钟不能快于最慢一级所需的时间。

流水线开销来自流水线寄存器延迟和时钟偏斜。寄存器会给时钟周期增加建立时间与传播延迟:建立时间指触发写入的时钟信号到来前,寄存器输入必须保持稳定的时间。时钟偏斜是时钟到达任意两个寄存器的时刻之差的最大值,也限制了时钟周期的下限。一旦时钟周期已经缩短到时钟偏斜与锁存器开销之和,继续加深流水线便没有意义,因为周期内已没有时间用于有效工作。感兴趣的读者可参见 Kunkel and Smith(1986)。

例题 考虑上一节的非流水线处理器。假定时钟频率为 4 GHz(或周期为 0.5 ns),ALU 操作和分支需要 4 个周期,访存操作需要 5 个周期;三者相对频率分别为 40%、20% 和 40%。由于时钟偏斜和建立时间,流水线化会给周期增加 0.1 ns 开销。忽略延迟影响,指令执行速率可以获得多大加速比?

解答 非流水线处理器的平均指令执行时间为:

平均指令执行时间=时钟周期×平均 CPI=0.5 ns×[(40%+20%)×4+40%×5]=0.5 ns×4.4=2.2 ns. \begin{aligned} \text{平均指令执行时间} &=\text{时钟周期}\times\text{平均 CPI}\\ &=0.5\ \mathrm{ns}\times[(40\%+20\%)\times4+40\%\times5]\\ &=0.5\ \mathrm{ns}\times4.4=2.2\ \mathrm{ns}. \end{aligned}

流水线实现的周期,必须等于最慢一级的时间加上开销,即 0.5+0.1=0.6 ns0.5+0.1=0.6\ \mathrm{ns};这也是平均指令执行时间。因此:

流水线加速比=非流水线平均指令时间流水线平均指令时间=2.2 ns0.6 ns≈3.7. \text{流水线加速比} =\frac{\text{非流水线平均指令时间}}{\text{流水线平均指令时间}} =\frac{2.2\ \mathrm{ns}}{0.6\ \mathrm{ns}}\approx3.7.

0.1 ns 的开销实际上限制了流水线的效果。如果开销不随时钟周期变化,Amdahl 定律告诉我们,它会限制加速比。

译注: 原页确实同时写“4 GHz”和“0.5 ns”,两者不对应;4 GHz 对应 0.25 ns。原解答始终按 0.5 ns 计算。

如果流水线中的每条整数指令都独立于其他指令,这条简单 RISC 流水线就能正常工作。实际指令可能相互依赖;这是下一节的主题。


  1. 译注: 此处原文写作“MIPS R40000”,与本段及 C.6 节的 R4000/4400 不一致。这里按明确的上下文写作 R4000。 ↩︎

  2. 译注: 原文说执行“三种功能之一”,随后实际列出四项。 ↩︎

表格阅读