流水线

一、 流水线的基本概念

  1. 定义: 流水线 (Pipelining) 是一种实现多条指令重叠执行的技术,类似于工厂的生产流水线。
  2. 核心思想: 将一个任务(如一条指令的执行)分解为若干个子步骤(流水线级),不同任务的子步骤在不同的硬件单元上并行处理。
  3. 洗衣店类比:
    • 非流水线: 洗完一批 -> 烘干一批 -> 折叠一批 -> 收拾一批,然后才开始下一批。
    • 流水线: 当第一批衣服从洗衣机转到烘干机时,第二批脏衣服就可以放入洗衣机;当第一批烘干转去折叠时,第二批进入烘干,第三批进入洗衣机。所有步骤(清洗、烘干、折叠、收拾)可以同时操作在不同的批次上。
  4. 流水线的优势:
    • 不缩短单任务时间: 对于单独一批衣服(或一条指令),总处理时间(延迟)并没有缩短。
    • 提高吞吐率: 由于多任务并行进行,单位时间内完成的工作量大大增加,即吞吐率 (Throughput) 得到改善。
    • 减少总工作时间: 对于大量任务,吞吐率的提升显著减少了完成所有工作的总时间。

二、 流水线的性能与加速比

  1. 理想加速比:
    • 条件:所有流水线步骤所需时间一样(平衡),且有足够的工作可做。
    • 公式:理想加速比 = 流水线级数
    • 例如:洗衣房4个步骤,理想加速比为4倍。MIPS 5级流水线,理想加速比接近5倍。
  2. 实际加速比的影响因素:
    • 流水线启动和排空: 当任务数量相对于流水线级数不是很大时,流水线未完全充满的启动阶段和逐渐结束的排空阶段会影响性能。
      • 洗衣店例子中,洗4批衣服只提高了2.3倍,因为流水线未完全充满。
      • 处理器例子中,执行3条lw指令,加速比为 2400ps / 1400ps ≈ 1.71,远小于4或5。
    • 流水线各阶段不平衡: 流水线的时钟周期必须满足最慢的操作步骤。
      • MIPS流水线中,即使寄存器读写只需要100ps,但ALU和存储器访问需要200ps,则流水线时钟周期必须是200ps。
    • 流水线开销: 流水线本身会引入一些额外开销(如流水线寄存器延迟)。
  3. 大量指令下的加速比:
    • 当执行指令数量巨大时,启动和排空开销被摊薄,实际加速比趋近于 非流水线单指令时间 / 实际流水线时钟周期
    • 例子中,增加1,000,000条指令后,加速比 ≈ 800ps / 200ps = 4.00。
    • 这说明,即使是5级流水线,由于阶段不平衡(最慢200ps),实际稳定加速比为4,而不是理想的5。
  4. 性能提升的本质: 流水线通过增加指令吞吐率实现性能提高,而不是减少单条指令的执行时间(延迟)。

三、 面向流水线的指令集设计 (以MIPS为例)

  1. 所有指令长度相同: 简化了取指和译码阶段。不像x86变长指令那样复杂(x86现代实现常将复杂指令转为类RISC简单操作再流水)。
  2. 很少的指令格式且源寄存器字段位置相同: 对称性使得译码和读寄存器可以同时进行,避免增加流水线级数。
  3. 存储器操作数仅出现在存取指令中 (Load/Store架构): 允许在执行级计算地址,下一级访问存储器。若支持直接内存操作,流水线会更复杂。
  4. 操作数内存对齐: 保证数据传输指令能在单个流水线级内完成存储器访问,避免一次访问需要多次操作。
  5. 每条指令最多只写一个结果且在流水线最后一级执行: 简化旁路(Forwarding)设计,避免了多结果写入或早期写入带来的复杂性。

四、 流水线冒险 (Hazards)

  • 定义: 在下一个时钟周期中,下一条指令不能按预期执行的情况。

  • 结构冒险 (Structural Hazard):

    • 原因: 硬件不支持多条指令在同一时钟周期执行(即缺乏足够的硬件资源导致冲突)。
    • 例子:
      • 洗衣烘干一体机(洗衣和烘干不能同时)。
      • 只有一个存储器端口,而IF阶段(取指)和MEM阶段(数据访问)同时需要访问。
      • 只有一个寄存器写端口,而多条指令同时到达WB阶段。
      • 不完全流水化的功能单元(如某些FPU)。
    • MIPS设计避免: MIPS指令集设计有助于避免结构冒险,但如共享存储器仍可能发生。
    • 解决: 暂停(Stall/Bubble),或增加硬件资源(如分离的指令/数据缓存、多端口寄存器堆、复制功能单元)。
  • 数据冒险 (Data Hazard):

    • 原因: 一条指令需要等待另一条(前面的)指令完成才能获取其产生的数据。
    • 例子:
      • 洗衣时发现袜子不成对,需要暂停去寻找。
      • add $s0, $t0, $t1 后紧跟 sub $t2, $s0, $t3sub指令需要add指令的结果$s0
    • 影响: 若不处理,add指令结果在第5级才写回,会导致sub指令等待多个周期。
    • 解决方法:
      • 前推 (Forwarding) / 旁路 (Bypassing): 从内部资源(如ALU输出、MEM级输出)直接将结果传递给后续需要该结果的指令的较早阶段(如EX级输入),而无需等待其写回寄存器堆。
      • 流水线阻塞 (Pipeline Stall) / 气泡 (Bubble): 当旁路也无法解决时(如“取数-使用型数据冒险” lw后紧跟使用其结果的指令,数据在MEM级才可用,对下一条指令的EX级来说太晚),必须插入暂停。
      • 编译器代码重排: 调整指令顺序,将不相关的指令插入到依赖指令之间,以避免或减少阻塞。
  • 控制冒险 (Control Hazard) / 分支冒险 (Branch Hazard):

    • 原因: 决策(通常是分支指令)的结果未确定时,流水线不知道下一条应该取哪条指令。
    • 例子:
      • 洗足球队服,需要根据脏污程度决定洗衣设置,但结果要等烘干后才知,后续洗衣步骤受影响。
      • 执行分支指令后,流水线已取了顺序下一条指令,但如果分支跳转,则取的指令是错误的。
    • 解决方法:
      • 阻塞 (Stall): 取分支指令后立即暂停流水线,直到分支结果确定。这会降低性能(如分支指令CPI增加)。
        • 若能在ID级解决分支,通常阻塞1个周期。
      • 预测 (Predict):
        • 静态预测: 总是预测分支不发生(或总是发生)。预测正确则全速运行,错误则阻塞并冲刷流水线。
        • 动态预测: 根据分支历史行为进行预测(如分支历史表)。准确率可达90%以上。预测错误时,同样需要冲刷流水线并从正确路径重新取指。
      • 延迟分支 (Delayed Branch): (MIPS早期采用) 分支指令后的一条或几条指令(延迟槽指令)总是被执行,无论分支是否跳转。编译器负责在延迟槽中放入有用的或安全的指令。对较长分支延迟效果不佳,现代处理器多用硬件预测。

五、 流水线小结与展望

  1. 对程序员的透明性: 流水线利用指令间并行性,通常对程序员是不可见的(与多处理器编程不同)。
  2. 性能关键因素: 流水线的有效运作(与存储系统一起)是决定处理器CPI和性能的最重要因素。
  3. 冒险的普遍性: 结构、数据、控制冒险在简单和复杂流水线中都至关重要。
    • 结构冒险: 常出现在浮点单元等难以完全流水化的地方。
    • 控制冒险: 在整数程序中更常见(分支多且难预测)。
    • 数据冒险: 整数和浮点程序都可能成为瓶颈,浮点程序由于分支少、访存规则,编译器更易优化。整数程序指针多、访存不规则,优化更难。
  4. 重点回顾: 流水线提高了指令吞吐率,但不减少单条指令的执行时间或延迟
  5. 后续内容: 更高级流水线技术(超标量、动态调度)、具体冒险处理实现、异常处理等。