流水线CPU
流水线核心概念
理念
- 将指令执行划分为多个时间均衡的子阶段,使得多条不同指令再不同阶段并行处理
处理方式
- 可以想象一下,假设在洗衣时,需要经过洗涤(30min)$\rightarrow$ 烘干 (40min)$\rightarrow$ 折叠(20min),现在有4人需要洗衣服务,如果全部依次处理(单周期CPU),那么耗时是6h,即处理每个人的服务需要90min(1.5h),依次执行,共计耗时6h。如果使用流水线处理,可以在执行上一个人的烘干任务时执行下一个人的洗涤任务。
- 可以看到通过通过流水线处理将总耗时缩减到了3.5h
- 与洗衣类似,指令执行也可以分为三个阶段IF(Instucction Fetch), ID(Instruction Decode), Ex(Execution)
- 在串行处理中,与上面的洗衣类似,下一条指令在上一个指令的三个阶段全部结束后才开始
- 此时的运行时间为$6Δt$ (这里为了方便演示,假设各个阶段的处理时间相同)
- 可以发现与洗衣类似,后一条指令并不需要等待前一条执行完毕,而是只需要对应的模块”空出来“就可以执行
- 此时的运行时间为$5Δt$
- 可以发现运行时间可以被进一步缩短,即增加重叠部分
- 此时的运行时间为$4Δt$
- 以上两种重叠方式分别被称为 单重叠(Single overlapping) 和 双重叠(Twice overlapping)
重叠方式比较
单重叠
- 优点
- 相较串行运行时间缩短近$\frac{1}{3}$(对大量指令)
- 功能单元利用率显著提升
- 缺点
- 需要额外硬件支持
- 控制过程复杂化
双重叠
- 优点
- 相较串行运行时间缩短近$\frac{2}{3}$(对大量指令)
- 功能单元利用率进一步显著提升
- 缺点
- 需要大量额外硬件支持
- 需要物理分离的fetch, decode和execution单元
注: 双重叠面临的问题和需要的硬件支持
核心问题:内存访问冲突
- 在双重叠中如果多条指令同时访问内存,会引发冲突
- 冲突场景:
实践场景:双重叠还原为单重叠
- 上面的讲解都是基于三个阶段耗时相等的假设的,但是在实际CPU场景中,三个阶段的运行时间并不相等,一般IF阶段耗时最少,如果IF阶段耗时很短可以忽略,那么双重叠在优化上就约等与单重叠了
阶段不等长——重叠中的资源浪费和冲突问题
- 在考虑到应用场景中各阶段不等长后,可以进一步考虑潜在的问题
- 如果ID < EX
- 可以看到此时一条指令的EX阶段在时间上与下一条指令的EX阶段发生了重叠,这被称之为资源冲突
- 如果ID > EX
- 可以看到此时的时间轴上存在未执行指令的部分,这被称为资源浪费
- 如果ID < EX
- 注:为什么是这种执行方式?
- 所有流水线阶段在同一时钟边沿同步推进,即IF始终是在每个时钟周期开始时触发的。ID > EX的情况下的实际过程是IF(K+1)在执行完毕后等待到ID(K)执行完毕,开启下一个时钟周期才开始执行ID(K+1)和IF(K+2)。这个现象被称为阻塞(Block)
流水线概念
核心解释
- 指令分解:将单条指令的执行划分成m个子阶段,要求m>5。经典设计为五级流水线。m被称为流水线深度
- 时间均等:要求每个子阶段耗时严格相等($Δt_{stage}$),由全局时钟周期$T_c$统一控制。若阶段耗时不等,以最慢阶段为基准设定($T_c$)
- 错位重叠:m条相邻指令在同一时间并行处理不同阶段
- 重叠方式参考上面的双重叠,实现全阶段并行
特征
结构特征
- 阶段划分:每阶段由专属功能单元实现(如IF/ID/EX)
- 时间均衡:最长阶段决定整体速度
- 这是流水线高效运行的关键,如果某个阶段时间比其他阶段长,这个阶段就会成为瓶颈(Bottleneck),如上面所演示的那样导致阻塞
- 流水线寄存器:缓存阶段见数据,隔离各阶段操作
- 传递数据:在时钟边沿将前一个阶段在本时钟周期内处理完成的结果捕获并储存起来
- 数据保持:在下一个时钟周期这个数据会被提供给下一个阶段作为输入,确保数据在正确的时间被后续阶段使用
- 阶段隔离:当前一个阶段在下一个时钟周期开始处理新任务时,后一个阶段使用的是寄存器中保存的、前一个阶段上一个周期的结果。没有这些寄存器,前一阶段的新输出会立即冲掉后一阶段还在处理的输入,导致数据混乱和错误。 它确保了每个阶段在一个时钟周期内可以独立地处理分配给它的那份工作(数据)。
适用场景:大量重复的顺序工作
- 大量:从上面的示例可以发现,在不考虑“开头”和“结尾”的情况下,当m=3时,运行时间应当是串行时间的$\frac{1}{3}$.但实际可以看到,存在开头和结尾的额外开销,这被称为流水线启动和排空,在稍后会涉及。同时不难发现,当处理条数更多时,运行时间更接近$\frac{1}{3}$,即-处理大量的指令时节省的时间可以稀释启动和排空开销
- 重复:任务的执行流程(分解成的阶段)是相似的。上面的演示中的阶段都是完全相同的,就是一种理想状态
- 顺序:任务见最好是顺序执行的,或相关性较低。如果任务间有较强的依赖性就容易导致阻塞
- 持续输入:保持流水线处于忙碌状态,避免空闲导致效率下降
时间参数:启动时间与排空时间
- 启动时间/首次延迟:从第一个任务进入流水线到离开的总时间
- 排空时间/排空延迟:从最后一个任务进入到所有流水线任务结束的总时间
流水线分类
按功能划分
- 单功能流水线
- 多功能流水线
按照并行性划分(针对多功能流水线)
- 静态流水线:多功能,但不支持混合任务。即同一时间段内智能固定配置为一种任务。切换任务需要排空当前流水线
- 动态流水线:支持混合任务
- 注:可以用咖啡机来做比喻。单功能流水线就是一台只能做美式咖啡的咖啡机。静态流水线可以做多种咖啡,但是每次切换口味需要清空管道。动态流水线可以同时制作多种咖啡。
按照运行顺序划分
- 顺序流水线:任务的流出和流入顺序相同,上面的演示都是顺序流水线
- 乱序流水线:任务的流出和流入顺序可以不同,允许先完成后面的任务
按硬件划分
- 部件级流水线/操作流水线:将处理器的算术逻辑运算部件(ALU)分段,使得各种类型的运算可以通过流水线方式执行。这是CPU内部针对单个复杂功能单元的流水化。例如,一个浮点乘法器可以被分成多个阶段(阶码处理、尾数处理、规格化等),从而让多个浮点乘法操作在内部重叠执行,提高该部件的吞吐率。
- 处理器级流水线/指令流水线:指令的解释和执行通过流水线实现。一条指令的执行过程被分成若干个子过程,每个子过程在一个独立的功能单元中执行。上面的演示都是指令流水线。RISC五级流水线就是一种指令流水线设计。
- 处理器间流水线/宏流水线:两个或更多处理器的连接,用于处理同一个数据流,每个处理器完成整个任务的一部分。常用于高性能计算或流处理系统
按照线性性划分
- 线性流水线:各阶段串行连接,没有反馈回路。数据每个阶段中在每个段最多流过一次
- 非线性流水线:存在反馈回路,允许数据流回前面的阶段再次处理
基于RISC-V的流水线CPU
RISC-V的流水线友好设计
- 所有指令都是32位
- 精简和规整的指令格式
- Load/Store架构
- 内存操作数强制对齐
流水线吞吐量
- 公式定义 $$TP = \frac{n}{T}$$ n: 处理的指令总数(任务数量) T: 完成任务的总时间 物理意义:单位时间内完成的指令数
- 性能上限约束 $$TP < TP_{max}$$ 含义:实际吞吐量永远低于理论极限值
- 实际运行时间 $$ T = (m+n-1) \times Δt_0 \newline TP = \frac{n}{T} = \frac{n}{(m+n-1) \times Δt_0}\newline TP_{max} = \frac{1}{Δt_0} $$ 可以发现当$n » m$时,有$TP \approx TP_{max}$ 可以写成 $$ TP = \frac{n}{n+m-1}TP_{max}$$
应用场景下的流水线吞吐量
- 如之前的演示所反映的,流水线在实际可能会遇到瓶颈问题。容易想到,这一问题可以通过将时钟周期设置为最慢阶段耗时来解决,但这并不能提高效率。为了实际提高效率有其他解法
- 解决方案
- 细分(Subdivision):将最长的阶段拆分为多个子阶段,每段耗时$Δt$
- 资源复制(Repetition):每$Δt$可开始一个新任务。这一解决方案实质上是在S2的内部执行并行加速。
更多性能衡量指标——Sp与η
- Sp(speed up) $$Sp = \frac{n \times m \times Δt_0}{(m+n-1)Δt_0} = \frac{n \times m}{m + n -1}$$ Sp衡量的是流水线相较串行加快运行速度的程度,当$n»m$,即输入数据很多时,有$Sp \approx m$,逼近上确界
- η(Efficiency) $$η = \frac{Sp}{m} = \frac{n}{m+n-1}$$ η的含义是实际加速比比理论最大加速比,当$n»m$,即输入数据很多时,有$η \approx 1$,逼近上确界
流水线冒险
在前面的演示中,已经看到了流水线中存在运行冲突的现象。事实上实践中可能发生的冲突情况较多,它们被统称为hazard(冒险)
冒险类型
数据冒险
- 以上的演示都未展示具体的指令内容和访问的对象,并且默认了各条指令间是独立的。但在实践场景中,指令间可能是关联的,这将引发数据冒险。如下:
|
|
- 可以看到第二条指令用到了第一条指令应在wb阶段写入的数据,流程图如下
- 可以看到在第一个指令完成WB之前就执行了第二个指令的ID,这会导致第二条指令无法去到正确的x1值。这种错误被称为读后写(RAW - Read After Write)
- 此外还有一种较为少见的冒险写后读(WAR - Write After Read)。如果我们按照上面的演示思路来思考,会发现似乎是不会出现写后读问题的,这是因为写后读事实上一般出现在乱序执行的处理器中。
|
|
- 在乱序执行的处理器中,可能有R2的WB在R1的ID前完成的情况,此时指令1读取r2值时读到的是被写入的新值而不是想要写的值。这种错误就是写后读
结构冒险
- 除去因为数据处理顺序的原因产生的冒险,如上面提到的,实践中可能存在不同指令的不同阶段访问相同硬件资源的问题,这也会引发冒险,被称为结构冒险
- 如图,存在两条指令的IF和MEM阶段重叠,同时访问内存,产生冒险
- 此外,同时访问只有一个写入端口的寄存器也会相似地产生结构冒险
控制/分支冒险
- 在上面的例子中,相邻指令都是物理相邻的,即下一条指令的地址即为上一条指令+4,可以在读上一条指令时即确定下一条指令地址。但是如果上一条指令是跳转指令,那么要等到上一条指令执行完,才能解析出下一条指令的地址,这种情况下就可能发生控制/分支冒险
- 可以看到在还不知道跳转指令的跳转地址时,后续指令已经执行了IF,这里的地址是预测性的,如果预测错误,在后续会被冲掉,这就是控制/分支冒险
解决冒险
数据冒险
- 旁路
- 原理:当指令的结果在流水线中计算出来后不等待其写回寄存器,而是直接用专门的数据通路(旁路)将结果转发给需要的后续指令
- 缺点:不能解决所有RAW,特别是当数据来自load指令时,数据在mem阶段菜可用,如果后续指令在mem前就需要,仍然会产生停顿
- 暂停/气泡
- 原理:当旁路无法解决冒险时,流水线控制器会插入一个或多个“气泡”(即空操作),强制延迟后续指令知道数据可用
- 缺点:引入停顿,降低了流水线的CPI(每条指令的时钟周期),从未降低了性能
- 寄存器重命名
- 原理:主要用于解决WAR(写后读)和WAW(写后写)这两种“假”数据依赖(或称名称依赖,Name Dependencies)。处理器为逻辑寄存器分配不同的物理寄存器,这样,不同的指令即使操作同一个逻辑寄存器,也可以写入到不同的物理寄存器,从而消除冲突,允许指令乱序执行。
结构冒险
- 复制资源
- 原理:为发生冲突的硬件增加额外的副本。这是最直接和有效的解决方案
- 示例
- 内存端口冲突:采用哈弗架构,提供独立的指令内存和数据内存,或者在cpu内部设置独立的指令缓存和数据缓存,从而允许同时访问
- 功能单元冲突:如果alu是瓶颈,可以增加多个alu,以便不同的指令可以并行使用
- 寄存器文件端口冲突:增加寄存器文件的读写端口数量,允许在同一周期内进行更多的读写操作
- 缺点:增加了硬件成本和复杂度
- 流水线暂停
- 原理:当发生结构冒险时,暂停其中一条指令,直到所需的资源可用
- 缺点:引入停顿,降低性能。通常仅作为复制资源的后备方式或在复制资源代价过高时使用
控制冒险
- 分支预测
- 通过复杂的硬件逻辑来预测分支的走向和目标地址
- 分类:
- 静态预测:基于编译时的信息和简单的经验法则
- 动态预测:基于历史行为来预测
- 延迟分支
- 原理:在分支指令后紧接着一条或几条分支延迟槽指令
- 缺点:填充难度较大,在现代高性能处理器较少使用
- 分支消除/条件执行
- 原理:对一些简单的条件操作,可以通过硬件支持,将条件分支转换为无需跳转的条件执行指令。
- 缺点:并非所有复杂的条件分支都能通过这种方式消除
流水线数据通路