CS61C – 6 Pipelining

这一部分对应的是 lec 21 ~ 23 (哔大上的参考课程为[Summer 20]),主要涉及的是计算机架构中最伟大的思想之一:流水线(Pipelining)。这标志着从单周期处理器的理解飞跃到了现代高性能处理器的设计核心。

流水线设计有着一个核心目标:大幅提升处理器的性能。(lec 21)而为了实现这一目标,计算机学家借鉴了工业上的流水线思想,将指令划分为多个阶段,让多条指令在同一时间片内处于不同的阶段,从而实现指令级并行(ILP)(lec 22)。但这同时也带来了新的问题(lec 23)——冒险(Hazards),讲座在最后为其提供了相关的解决方案,包括转发(Forwarding/Bypassing)流水线停顿(Stalling) 和分支预测(Branch Prediction)

性能衡量指标 

如何对处理器的性能进行评估?lec 21 给出了一些指标,具体包括:

  • 执行时间 (Time):完成一个任务所花费的总时间。这是最直接的指标。
  • 吞吐率 (Throughput):单位时间内完成的任务数量。对于服务器等场景更重要。
  • CPU 时间公式: $Time = Instructions \cdot CPI \cdot Cycle Time$
    • Instructions:由程序、算法、编译器和ISA决定。
    • CPI (Cycles Per Instruction):由处理器微架构(如是否流水线)决定。
    • Cycle Time:由工艺技术和关键路径决定。流水线通过切割关键路径来缩短 Cycle Time

注意 CPU 时间公式, 理解这个公式是分析任何计算机性能的基础。优化性能就是优化这三者。单周期处理器 CPI=1 Cycle Time 很长。流水线目标是保持 CPI≈1 的同时,大幅缩短 Cycle Time

流水线

概念

前面杂七杂八说了这么多,如何去理解流水线?讲座以洗衣房为例,指明流水线的核心在于时间上的并行。从cpu的角度出发,流水线就是将指令处理过程划分为多个阶段(Stage),每个阶段由一个专门的硬件单元(Unit)负责,多条指令重叠执行,当多条指令运行时处于阶段,总的吞吐率就远高于顺序执行。

理想情况下,对于一个 k 级流水线,加速比接近 k 倍。但由于流水线填充和排空的开销,实际加速比小于 k 。因为系统需要以执行时间最长的阶段为准对齐颗粒度。

所有需要注意的是,流水线虽然提高了吞吐率,但并没有减少(甚至可能增加)单条指令的延迟(Latency)。它的优势在处理大量指令时才能体现出来.

实现

提到流水线的实现,那么必然离不开流水线寄存器。这是实现流水线的关键硬件。它们位于各个阶段之间(如IF/ID, ID/EX, EX/MEM, MEM/WB),用于传递当前指令在该阶段处理后的所有信息(数据和控制信号)到下一个阶段。

有了流水线寄存器,我们就可以将每个阶段分割出来确保每个阶段都在处理一条指令的特定部分,避免不同指令的数据混叠。

但分割出来必然不会全是好处,事实上,如果指令间存在依赖关系并行执行就会出很大纰漏.

这部分有点类似于 DBS 中的事务控制,常见的错误如脏读、幻读、不可重复读等都会在并发过程中出现,不过这块我们换了个名称称呼,称之为冒险(Hazard)

流水线冒险 (Hazards)

结构冒险 (Structural Hazard)

这种冒险来源于硬件资源与并发的冲突,例如,单端口内存无法同时被IF阶段取指和MEM阶段访存。

所以解决方法很简单——增加硬件资源。除此以外,现代处理器采用独立的指令缓存(I-Cache)数据缓存(D-Cache)来根本性地避免此问题。RISC – V ISA 本身也通过简化指令(每条指令至多一次访存)来帮助避免结构冒险。

数据冒险 (Data Hazard)

这就类似于上文引用块中我们提及的脏读,即因数据依赖而无法获得正确的操作数,但与 DBS 中的锁机制不同,处理数据冒险有着三种解决方案:

  • 停顿 (Stalling):插入“气泡”(Bubble/NOP),让流水线空转,等待前一条指令计算出结果并写回。该方法最易想到但性能损失大
  • 转发 (Forwarding / Bypassing)最重要的概念之一。将计算结果直接从产生它的阶段(如EX/MEM或MEM/WB寄存器)回传到需要它的阶段(EX阶段的ALU输入)。无需等待写回寄存器文件,因而极大减少了停顿。
  • 在部分情况下我们无法进行转发,比如前一条依赖是 lw 因为LW指令的数据在MEM阶段结束时才从内存中读出而下一条指令可能在 EX 阶段就需要它。此时必须停顿一个周期(称为Load-Use Hazard),然后再结合转发。但编译器可以通过指令调度(Code Scheduling) 在两条依赖指令之间填充其他和该操作无关的指令,来避免这种性能损失。

控制冒险 (Control Hazard)

所控制冒险,是指因分支指令改变程序流而导致预取的下一条指令无效.因为在ID阶段才能解析出分支是否跳转,但此时后面两条指令(IF、ID)已经被预取,如果要跳转则相当于此时的 IF 和 ID 是无效操作/

当然我们同样可以考虑空(Flush)分支指令之后的所有指令(将其转为NOP)并等待分支结果。但代价是每次分支都带来2个周期的停顿,这将会是性能灾难。

所以现代处理器采用了一种更高效的处理方法——分支预测 (Branch Prediction):在流水线更早的阶段(甚至取指时)就预测分支是否会跳转,并根据预测结果继续取指。如果预测正确,则性能无损;如果预测错误,再清空流水线,代价与停顿相同。

当然如何进行分支预测讲座没提,这需要在后续课程中再进行探讨


在讲座的最后,Prof.Bora 提到了一些延伸:

  • 更深流水线:将阶段划分得更细(如10级、15级),进一步缩短时钟周期,提升频率。但会加剧冒险问题(特别是控制冒险)。
  • 多发射(超标量 Superscalar):每个时钟周期从流水线中发射(Issue)多条指令(如4条),需要复制大量的硬件单元(多个ALU,多个译码器等)。此时CPI < 1,常用IPC (Instructions Per Cycle) 来衡量性能。
  • 乱序执行 (Out-of-Order Execution):硬件动态调整指令的执行顺序,以克服数据冒险,充分利用执行单元。

这些技术都是为了从不同角度挖掘指令级并行(ILP)以榨取更高性能。

写在最后

从单周期到流水线,本质是从串行执行变为并行执行。计算机架构的进化史就是一部挖掘各种层次并行(指令级、线程级、数据级)的历史。为了实现并行,我们引入了流水线,而流水线虽然带来了吞吐率的巨大提升,但同时也引入了复杂性。冒险检测与解决逻辑(转发网络、冒险检测单元、分支预测器)使得控制逻辑变得异常复杂。这是一种典型的以设计复杂性换取性能的权衡(Trade-off)

在设计流水线时,阶段之间的接口(流水线寄存器)是严格定义的。这允许每个阶段的设计相对独立,只要遵守接口规范即可。这是“层次抽象”的又一次体现。

RISC – V ISA的规整性(等长指令、简单寻址模式、少数的寄存器端口需求)是它能高效实现流水化的根本原因。反之,CISC 的ISA(如x86)需要更复杂的硬件来解码和映射到内部的类RISC微操作(µops)。同时,编译器可以通过指令调度来避免数据冒险,减轻硬件负担。

以上

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注