算力芯片:高性能CPU/GPU/NPU微架构分析
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.2.1 经典5级流水线概述

最初的5级流水线设计源于MIPS体系结构,旨在通过分离不同的指令操作来提高性能,对应的5个阶段是取指、指令译码、执行指令、访存取数、结果写回,这5个阶段分别对应了一个指令在CPU中执行的5个主要步骤。这是一个理想化的指令周期,旨在理想情况下尽可能地减少指令之间的依赖关系,这样可以更容易地并行处理指令。图2-2为经典的MIPS处理器5级流水线。

图2-2 经典5级流水线

● 取指(Instruction Fetch):这是每个指令执行的第一步,即从内存中获取指令。在这个阶段,CPU从内存中读取下一个要执行的指令。这个阶段通常涉及程序计数器,它保存了内存中下一个指令的地址。在取指阶段结束后,指令被放入指令寄存器,等待下一阶段的处理。

● 指令译码(Instruction Decode):取出的指令被译码,也就是识别出这是什么指令,需要进行什么操作。在这个阶段,CPU解析刚刚取得的指令,理解它要做什么。这涉及把原始的指令编码(一般是二进制代码)转化为 CPU 能理解的操作和操作数。在译码后,指令被拆分为一系列微指令,这些微指令直接映射到CPU的硬件级操作。

● 执行(Execute)指令:根据译码的结果,进行相关的计算或者逻辑操作。在这个阶段,CPU 根据指令译码阶段得到的信息执行具体的操作。这可能涉及使用算术逻辑单元(Arithmetic Logic Unit,ALU)执行数学运算,而且整数和浮点数的流水线在这里是分离的,因为具体的整数或浮点数单元在物理上是隔离的,流水线长度也不同,涉及其他特定的功能部件执行特定的操作。

● 访存(Memory Access)取数:如果指令需要读取或者写入内存,就会进行这个阶段。例如,load和store指令就会在这个阶段进行内存的读/写。在这个阶段,如果执行的指令需要访问主存储器(如加载或存储数据),则CPU将完成这个操作,这可能涉及对缓存的访问。如果数据不在缓存中,则可能需要从RAM中获取。

● 结果写回(Write Back):将执行的结果写回寄存器中。在这个最后的阶段,执行结果被写入指定的寄存器或者内存中。执行结果可能是一个计算的结果,也可能是一个内存读取的结果。

这样划分的目的是让每个阶段的工作量尽可能相似,从而实现每个阶段在一个时钟周期内完成,提高处理器的工作效率。当然这只是一个理想的模型,在实际的处理器设计中,流水线的阶段可能会更多,这些年主流的CPU 已经拥有了远超5级流水线的长度,例如 Intel的Core系列和AMD的Zen系列处理器的流水线都在15~25级。不同的处理器会根据自身的设计目标和工艺限制进行更细致的流水线划分,这种超过经典5级技术的流水线叫作超流水线。

在这样的流水线设计中,程序计数器(Program Counter,PC)用于追踪当前正在取指的指令地址。图2-2中的Next SEQ PC通常表示“下一个顺序化的程序计数器值”。在大多数指令集架构中,指令是顺序执行的,除非有分支、跳转或其他控制转移指令改变了这种顺序。因此,“顺序”意味着按照指令的正常、连续顺序执行。

那么为什么Next SEQ PC会连接译码和执行单元呢?这是因为分支或跳转指令的目标地址往往在译码或执行阶段确定。例如在译码阶段,分支指令可能会进行条件检查,并决定是否跳转。如果需要跳转,则流水线必须更新PC以取得新的指令地址。这会导致流水线冒险,尤其是在分支预测失败时。为了解决这种冒险,流水线中经常使用分支预测技术和/或延迟分支槽来预测或消除分支延迟。

执行单元中的zero代表了用来检查分支指令的条件分支单元。在许多指令集架构中,一些分支指令的行为取决于特定寄存器值是否为零。例如,MIPS架构中的BEQ(Branch if Equal)和BNE(Branch if Not Equal)指令会检查两个寄存器值是否相等,若相等(或不相等)则执行分支。图中的“zero”和“?”放在一起,意思是一个条件检查“Is the value zero?”或者“Is the condition met?”,当“zero”检查返回真时,“branch taken”信号会被激活,分支条件满足,流水线应该跳转到分支指定的目标地址,否则流水线将继续顺序执行下一个指令。

我们会注意到,取指和指令译码都围绕指定的解析调度做文章,这个时候狭义的计算(加、减、乘、除和其他复杂计算)还没有开始,算术逻辑单元还没有介入工作。所以我们习惯将这两个阶段命名为前端(Front End),将后3级流水线习惯性命名为后端(Back End)或者执行单元(Execution Engine),后端主要的工作都是计算和访存。

CPU的前端:前端的任务主要是从程序代码中获取指令并将其译码为执行单元可以理解的操作。前端通常包含以下部分。

● 指令预取(Instruction Profetch):从内存中获取指令的部分。

● 分支预测(Branch Prediction):预测程序的控制流可能如何更改,以便提前获取正确的指令。

● 指令缓存(Instruction Cache):存储最近或即将使用的指令,以减少访问内存的时间。

● 指令译码(Instruction Decode):将获取的指令转换为一系列微指令或者硬件能理解的指令。

CPU的后端:后端的任务主要是执行从前端发送过来的指令,并将结果写回内存或寄存器。后端通常包含以下部分。

● 执行单元(Execution Unit):进行实际的运算和操作,如算术逻辑单元(ALU)和浮点数单元(FPU)等。

● 加载/存储单元(Load/Store Unit):负责处理涉及内存的操作,比如从内存中取值或者向内存写入值。

● 写回单元(Write Back Unit):将计算或取值的结果写回指定的寄存器或内存。

在超标量和超流水线的设计中,这种前后端的划分可以帮助实现更高的并行度。例如,前端可以预取和译码多个指令,同时后端可以并行地执行这些指令,这样就可以在一个时钟周期内完成更多的工作,提高处理器的性能。同时这种划分也为乱序执行和指令级并行技术提供了可能。

MIPS架构的设计目标是每一个流水线阶段对应一个时钟周期,一个指令从取指到完成,需要经过5个时钟周期。在理想状态下(没有发生冒险、分支预测准确等),CPU可以每个时钟周期完成一个指令的执行,这就是所谓的“一周期一指令”。

上述讨论涉及以下几个术语。

● 平均指令周期数CPI(Cycle Per Instruction):表示执行某个程序的指令的平均周期数,可以用来衡量计算机运行速度。

● 每个时钟周期内的指令数IPC(Instructions Per Clock/Cycle):表示CPU每个时钟周期内执行的指令数。需要注意CPI和IPC讨论的都是指令级别的吞吐能力,并不是数据,不代表CPU最终的整数或浮点算力。

● 时钟周期:也称为振荡周期,定义为时钟频率的倒数,在一个时钟周期内,CPU仅完成一个最基本的动作,是计算机中最基本的时间单位。

CPU执行时间=时钟周期数/时钟频率=指令个数/IPC×时钟频率

举例来说,假设我们有4个指令I1、I2、I3、I4,需要依次执行,如果没有流水线,则可能需要4个时钟周期才能完成I1的执行,然后用4个时钟周期完成I2的执行,以此类推,总共需要16个时钟周期完成这4个指令。但是如果使用了流水线,那么当I1执行到第二个阶段时,I2就可以开始执行第一阶段,以此类推,那么我们只需要7个时钟周期就可以完成这4个指令的执行,大大提高了CPU的指令吞吐率。