非流水线CPU¶
中央处理器概述¶
- CPU执行一条指令的过程
- 取指令
- PC+“1”送PC
- 指令译码
- 进行主存地址运算
- 取操作数
- 进行算术 / 逻辑运算
-
存结果
-
CPU 由 执行部件(数据通路) 和 控制部件(控制器)组成
- 执行部件:操作元件(ALU)、状态存储元件
- 控制部件:译码部件、控制信号生成部件、状态存储元件
- 控制器负责对执行部件(数据通路)发出控制信号
数据通路基本结构¶
-
数据通路由操作元件(组合原逻辑元件, 如加法器、多路选择器、译码器等)和存储元件(时序逻辑元件)通过总线或分散方式连接而成的进行数据存储处理和传送的路径、
-
存储元件
- 寄存器(组)
- 两个读口(组合逻辑操作):busA和busB分别由RA和RB给出地址。地址RA或RB有效后,经一个“取数时间(AccessTime)”,busA和busB有效。
- 一个写口(时序逻辑操作):写使能为1的情况下,时钟边沿到来时,busW 传来的值开始被写入 RW 指定的寄存器中。
- 理想寄存器
- 为简化数据通路操作说明,把存储器简化为带时钟信号Clk的理想模型。并非真实存在于CPU中!
- 读操作(组合逻辑操作):地址Address有效后,经一个“取数时间AccessTime”,Data Out上数据有效。
- 写操作(时序逻辑操作) :写使能为1的情况下,时钟Clk边沿到来时,Data In传来的值开始被写入Address指定的存储单元中。
CPU的性能评估¶
- 时钟周期:Cycle Time = Latch Prop + Longest Delay Path + Setup + Clock Skew
- 约束条件:(Latch Prop + Shortest Delay Path - Clock Skew) > Hold Time
- Latch Propagation Delay: 从锁存器捕获数据到数据稳定输出所需要的时间。
- Longest Delay Path: 在触发器或锁存器输出数据后,数据在电路中传输所遇到的最长延迟。(组合逻辑电路延时)
- Setup Time: 下一个触发器或锁存器在时钟沿到来之前,数据必须稳定的时间。
-
Clock Skew: 时钟在分布到不同触发器或锁存器时的最大时间差异。
-
性能的衡量
- 响应时间/时延
-
吞吐率、带宽
-
CPI
- CPU 执行时间=CPU 时钟周期数/程序*时钟周期=指令条数/程序*CPI*时钟周期
- 对于某一条特定的指令而言,其CPI是一个确定值——与CPU设计有关。
- 但是,对于某一个程序或一台机器而言,其CPI是一个平均值,表示该程序或该机器指令集中的1条指令执行时平均需要多少时钟周期。
-
平均CPI的计算:
- \(CPI_i\)表示第\(i\)类指令的\(CPI\),\(C_i\)表示该指令的指令条数
- 总时钟数\(\sum^n_{i=1}CPI_i\cdot C_i\)
- CPU时间就是 时钟周期乘以总时钟数
- 综合\(CPI=总时间周期数/指令条数\)
- 用\(F_i\)表示指令出现得概率,\(CPI=\sum^n_{i=1}CPI_i\cdot F_i\)
-
指令数目、CPI、时钟周期
- 指令数目由编译器和ISA决定
- CPI由ISA和CPU的实现来决定
-
时钟周期由CPU的实现来决定
-
产品宣称指标(每秒运算次数)
- \(MIPS= Clock Rate / CPI *x* 1/10^6\)
- 表示定点指令的执行速度
- \(MFLOPS = FP Operations / Second *x* 1/10^6\)
- 算术平均:求和后除n
- 根据算术平均执行时间能得到总平均执行时间
-
几何平均:求积后开根号n
- 根据几何平均执行时间不能得到程序总的执行时间
-
执行时间的规格化(测试机器相对于参考机器的性能):参考机器上执行时间÷ 待测机器上执行时间
- 对于同一个机器上跑的多个程序应该使用几何平均
- 如:程序1 从2秒变成1秒 , 程序2 从2000秒变成1000秒
- 算术平均提升 = (1+1000)/2=500.5(1+1000)/2=500.5 秒
- 这个结果看起来像是一个巨大的性能差异,但实际上每个程序的性能提升都是100%。算术平均没有考虑到每个程序性能提升的相对比例。
- 几何平均提升 = 1×1000=31.621×1000=31.62 秒
- 这个结果更好地反映了每个程序相对于其自身的性能提升。几何平均通过考虑每个比率的相对大小,提供了一个更准确和公平的性能提升衡量方式。
单周期数据通路的设计¶
RISC-V指令格式¶
- 寄存器有32个32位寄存器(0号始终为0),寄存器编号占5位
- 存储器只能通过 load 及 store 指令访问
重点¶
- 每条指令的第一步都是取指令并PC加4,使PC指向下条指令
- 注意内存对齐,RESIC-V中的地址都是偶数,因此关于PC的地址对立即数乘以2
- 数据不要求按边界对齐,执行到一条不按边界对齐的访存指令时,硬件抛出异常,由软件进行处理
- 由此分析数据通路需要的组件:
- 立即数扩展器
- ALU
- 取指令部件
扩展器部件¶
- 根据指令格式对指令中的立即数进行拼接和扩展,形成32位立即数
- ExtOp:控制选择和输入指令相匹配的立即操作数输出
- 通过指令译码器得到
- assign immI = {20{Instr[31]}, Instr[31:20]};
- assign immU = {Instr[31:12], 12'b0};
- assign immS = {20{Instr[31]}, Instr[31:25], Instr[11:7]};
- ssign immB = {20{Instr[31]}, Instr[7], Instr[30:25], Instr[11:8], 1'b0};
- assign immJ = {12{Instr[31]}, Instr[19:12], Instr[20], Instr[30:21], 1'b0};
算数逻辑部件¶
- 使用OPctr和一些标志位(如符号标志位,减法标志位)来表示运算的类型
- 由指令得到ALUctr,再进一步解析得到OPctr和标志位
取指令部件¶
- 每条指令的公共指令
- 取指令: M[PC]
- 更新PC:PC ← PC + 4
- 先取指令,再改PC的值
数据通路¶
- R-型指令的数据通路
- I-型运算指令ori的数据通路
- U-型指令的数据通路
- Load指令的数据通路
- Store指令的数据通路
- B-型指令的数据通路
- 下地址逻辑
- J-型指令的数据通路
- 完整数据通路
单周期控制器的设计¶
控制通路¶
- 所有指令开始时的动作:取指令: Instruction ← M[PC]
- R指令的操作流程
- I指令操作过程
- U型指令
- Load
- Save
- B指令
- J指令
- lw指令的执行时间最长, 它所花时间作为时钟周期
- 单周期处理器的CPI为1,但是由于以最复杂的指令的执行时间作为时钟周期,频率比较低(很多指令本来可以以更短的时间完成,这造成了浪费)
多周期处理器的设计¶
- 单周期与多周期的比较
- 成本比较
- 单周期下功能部件不能重复使用;而多周期下可重复使用,比单周期省
- 单周期指令执行结果直接保存在PC、Regfile和Memory;而多周期下需加一些临时寄存器保存中间结果,比单周期费
-
性能比较
- 单周期CPU的CPI为1,但时钟周期为最长的load指令执行时间
- 要注意:多周期的性能不一定比单周期好!关键就在于划分阶段是否均匀。
-
把指令的执行分成多个阶段,每个阶段在一个时钟周期内完成
- 时钟周期以最复杂阶段所花时间为准
- 尽量分成大致相等的若干阶段
-
规定每个阶段最多只能完成1次访存或寄存器堆读/写或 ALU 运算
-
优点
- 时钟周期短(不同指令所用周期数可以不同)
-
允许功能部件在一条指令执行过程中被重复使用
-
指令执行阶段划分(前两个阶段所有指令固定相同)
- 取指令阶段
- 执行一次存储器读操作
- 读出的内容(指令)保存到寄存器IR(指令寄存器)中
- IR的内容不是每个时钟都更新,所以IR必须加一个“写使能”控制
- 在取指令阶段结束时,ALU的输出为PC+4,并送到PC的输入端,但不能在每个时钟到来时就更新PC,所以PC也要有“写使能”控制
- 译码/读寄存器堆阶段
- 经过控制逻辑延迟后,控制信号更新为新值
- 执行一次寄存器读操作,并同时进行译码
- 期间ALU空闲,可以考虑“投机计算”地址
- ALU运算阶段
- ALU运算,输出结果一定要在下个时钟到达之前稳定
- 读存储器阶段
- 由ALU运算结果作为地址访问存储器,读出数据
-
写结果到寄存器
- 把之前的运算结果或读存储器结果写到寄存器堆中
-
*多周期处理器的设计
各类指令执行过程分析¶
- (公共操作)取指令并计算下条指令地址(IFetch)
- 根据PC读指令并保存到IR,PC+4
-
(公共操作)译码并取数(Rfetch/ID)
- IR的OP及CC送控制器译码,并根据Rs和Rt读取寄存器中数据
- 加法器空闲,投机计算主存地址(但是只会在对主存操作时才用的上)
- 若译码发现是Load/Store指令则投机成功,使得Load/Store指令减少一个时钟周期;若不是,则只要保证MAR不送总线即可(MARout=0)
-
R型指令
- R-型指令的执行,需要两个时钟周期,记为 RExec、RFinish状态
- 进行ALU运算并将结果存入ALUout
- 将相应结果分别写入Rt和CC寄存器
-
为什么要拆分为两个阶段并且使用ALUout寄存器
- 如果两个周期合并,可能来不及把异常结果转去进行异常处理,就可能把错误结果写入寄存器了
-
I型指令
- I-型运算指令的执行,需要两个时钟周期,记为 IExec、IFinish状态
- 与R型类似,进行ALU运算并将结果存入ALUout,再将相应结果分别写入Rt和CC寄存器。
-
Load指令
- 地址已投机计算,还需两个时钟周期,记为 lwExec、lwFinish状态
- 根据投机计算好的地址(MAR中)到主存中取数,送MDR
- 将MDR内容写入Rt。
-
两个阶段可以合并,但是会导致时钟周期变长
-
Store指令
- 地址已投机计算,还需两个时钟周期,记为 swExec、swFinish状态
- 将Rt的内容写入MDR
- 将MDR的内容写入投机计算好的主存单元中(地址在MAR中)。
-
Jump指令
- 计算转移目标地址(PC+SEXT(imm6))并送PC,需一个时钟周期,记为 JFinish状态
-
时序图
- 状态元件内容和控制信号取值的改变(蓝色线):在时钟到来后的一定延时后发生
多周期控制器¶
- 本质上一个阶段对应的就是一组控制信号
- 状态转化图
-
单周期 CPU 中,控制信号在整个指令执行过程中不变,用真值表能反映指令和控制信号的关系。根据真值表就能实现控制器!但是多周期中不同阶段的控制信号不是固定的。
-
多周期控制器功能描述方式:
- 有限状态机:用硬连线路(PLA)实现
- 微程序:用ROM存放微程序实现
有限状态机PLA实现¶
- 由时钟、当前状态和操作码确定下一状态。不同状态输出不同控制信号值
- 状态转换图中,共10个状态,分别为0~9,故状态寄存器至少需4位
- 下一状态是当前状态和操作码的函数。每来一个时钟,当前状态变到下一个状态
- 硬布线方式实现
- 优点:速度快,适合于简单或规整的指令系统
- 缺点:它是一个多输入/多输出的巨大逻辑网络。对于复杂指令系统来说,结构庞杂,实现困难;修改、维护不易;灵活性差。甚至无法用有限状态机描述!
微程序实现¶
- 思想:仿照程序设计的方法,编制每个指令对应的微程序
- 每个微程序由若干条微指令构成,分别和各状态对应
- 每条微指令包含若干条微命令,分别和状态中的控制信号对应(一个微指令对应一系列控制信号,一个微命令控制一个控制信号)
- 所有微程序放在只读存储器 ROM 中(称为控制存储器 Control Storage,简称控存 CS ),都是0/1序列
- 特点:具有规整性、可维性和灵活性,但速度慢。
- 机器指令的执行过程
- 从CS中取出对应微程序
- 执行微程序,就是执行其中的各条微指令
- 对微指令译码就是产生对应的微命令——控制信号
-
微指令格式的设计
- µOP: 微操作码字段,产生微命令(控制信号) ;
- µAddr(配合常数):微地址码字段,产生下条微指令地址。
-
编码方式
-
下条微地址的确定方式
- 微指令地址的产生方法有两种
- 增量(计数器)法:下条微指令地址隐含在微程序计数器μPC中。
- 断定(下址字段)法:在本条微指令中明显指定下条微指令地址。
-
选择下条要执行的微指令有以下四种情况
- 取指微程序首址:每条指令执行前,CPU先执行取指微程序。
- 第一条微指令:每条指令取出后,必须转移到该指令对应的第一条微指令执行。
- 顺序执行时:微程序执行过程中顺序取出下条微指令执行。
- 分支执行时:在遇到按条件转移到不同微指令执行时,需要根据控制单元的输入来选择下条微指令。
*多周期中的异常处理机制¶
- 异常基本处理
- 关中断
- 保护断点和程序流程
-
识别异常事件
-
软件识别异常事件(RISC-V、MIPS等采用)
-
设置一个异常原因寄存器(如 RISC-V 和 MIPS 中的 Cause 寄存器),用于记录异常原因。操作系统使用一个统一的异常处理程序,该程序按优先级顺序查询异常状态寄存器,识别出异常事件。
-
硬件识别(I 386)
- 每个异常和中断都有一个异常/中断号,根据此号,到中断向量表(中断描述符表)中读取对应的具体的中断服务程序的入口地址。(特定程序处理特定异常)
带异常处理的多周期数据通路设计¶
- 寄存器
- EPC:32位,用于存放断点(异常处理后返回到的指令的地址)。
-
Cause:32位(有些位还没有用到),记录异常原因。
- 常见异常原因:未定义指令(Cause=1)、溢出(Cause=2)
-
“写使能”控制信号
- EPCWr:在保存断点时该信号有效,使断点PC写入EPC。
- CauseWr:在处理器发现异常(如:非法指令、溢出)时,该信号有效,使异常类型被写到Cause寄存器。
- 需要一个控制信号CauseMUX来选择正确的值写入到Cause中
- 需要将异常查询程序的入口地址(假设为0x10000)写入PC,可以在PC输入端增加一个MUX(控制信号PCMUX),其中一个输入为0x10000
- 假设只要处理两种故障类异常:未定义指令(Cause=1)、溢出(Cause=2)
- 故障类异常的断点为:PC减4。因为状态0(IFetch)时,已执行PC加4
- Cause寄存器的两个输入端,分别是 1(未定义指令时)和 2(溢出时)
- PC中将设置异常处理程序的首地址(假设为0x10000)
- 若发生异常,则Add1MUX=0, Add2MUX=10,EPCWr=1, CauseWr=1, PCMUX=0, PCWr=1,其他写使能信号都为0,
-
发生未定义指令时CauseMUX=0,发生溢出时CauseMUX=1。
-
带异常处理的控制器设计
- 在有限状态机中增加异常处理的状态,每种异常占一个状态
- 每个异常处理状态中,需考虑以下基本控制
- Cause寄存器的设置
- 计算断点处的PC值(PC-4),并送EPC
- 将异常查询程序的入口地址送PC
- 如何检测是否发生了这两种异常?
- 未定义指令(非法操作码):当指令译码器发现op字段是一个未定义的编码时
- 溢出:当R-型或I-型运算类指令在ALU中执行后,在条件码寄存器CC中的标志O为1时
流水线CPU¶
流水线处理器设计¶
- 指令的阶段划分(以 load 为例)
- Ifetch (取指及 PC 自增) : 取指令并计算 PC+4
- 指令存储器、Adder
- Reg/Dec (取数和译码) : 取数同时译码
- 寄存器堆读口、指令译码器
- Exec (执行) : 计算内存单元地址 (流水线中不使用投机,因为 ALU 一直被占用,不存在空闲造成的浪费)
- 扩展器、ALU
- Mem (读/写存储器) : 从数据存储器中读写
- 数据存储器
-
Wr(写寄存器): 将数据写到寄存器中
- 寄存器堆写口
- 每个周期有五个功能部件同时在工作
- 后面指令在前面完成取指后马上开始,吞吐率提高许多,理想情况下:每个周期有一条指令进入流水线,有一条指令完成,即CPI为1
流水线指令集的设计¶
- 对指令集的特征要求:规整、简单和一致
- 指令长度尽量一致,有利于简化取指令和指令译码操作
- 格式少,且源寄存器位置相同,有利于在指令未知时就可取操作数
- load / Store指令才能访存,有利于减少操作步骤,规整流水线
-
内存中”对齐”存放,有利于减少访存次数和流水线的规整
-
R-type
add rd, rs1, rs2
- Ifetch: 取指令并计算PC+4(写入PC)
- Reg/Dec: 从寄存器(rs1和rs2)取数,同时指令在译码器进行译码
- Exec: 在ALU中对操作数进行计算
- Mem:Nop(填充空指令)
- 如果不使用控制零作为占位,虽然指令执行更快,但是可能出现 冒险(两条指令试图同时写寄存器)
- 一个功能部件被多条指令同时使用称为结构冒险
- 因此要填充指令为相同阶段数目,并且保证每个功能部件每条指令只能用一次、每个功能部件必须在相同的阶段被使用
- 把所有指令都按照最复杂的“load”指令所需的五个阶段来划分,不需要的阶段加一个“NOP”操作
-
Wr: ALU 计算的结果写到寄存器(rd)
-
Store
sw rs2, rs1(imm12)
- Ifetch:取指令并计算PC+4 (写入PC)
- Reg/Dec: 从寄存器(rs1)取数,同时指令在译码器进行译码
- Exec:12位立即数(imm12)符号扩展后与寄存器值( rs1 )相加,计算主存地址
- Mem:将寄存器(rs2)读出的数据写到主存
-
Wr: Nop
-
Load:具有完整的非空的 5 个阶段
-
I-type
ori rd, rs1,imm12
- Ifetch:取指令并计算PC+4 (写入PC)
- Reg/Dec: 从寄存器(rs1)取数,同时指令在译码器进行译码
- Exec:使用ALU完成12位立即数(imm12)符号扩展后与寄存器值( rs1 )的运算(or)
- Mem:Nop
-
Wr: ALU 计算的结果写到寄存器(rd)
-
U-type
lui rd, imm20
- Ifetch:取指令并计算PC+4 (写入PC)
- Reg/Dec: 指令在译码器进行译码
- Exec:将20位立即数(imm20)末尾补0后形成32位数据直接送到ALU输出端
- Mem:Nop
-
Wr: ALU 输出端的结果写到寄存器(rd)
-
Beq
beq rs1, rs2, imm12
- Ifetch: 取指令并计算PC+4 (写入PC,但后续可能需要修改PC)
- Reg/Dec:从寄存器(rs1,rs2)取数,同时指令在译码器进行译码
- Exec: 执行阶段,ALU中比较两个寄存器(rs1,rs2)的大小(做减法)/Adder中计算转移地址(PC+SEXT(imm12)<<1)(使用两个加法器)
- Mem: 如果比较相等, 则:转移目标地址写到PC
- 为什么写入Mem要放在Mem阶段:
beq
是一种分支指令,它的作用是根据两个寄存器的值是否相等来决定程序的执行流程是否跳转到另一个地址。这个决策需要在流水线的早期阶段做出,以避免执行不必要的指令。 - 如果等到写回(WB)阶段才决定是否跳转,那么在此之前的所有阶段都可能已经加载了一些不必要的指令。这将导致效率低下,因为流水线可能需要清空并重新从新的地址开始加载指令。
- 为什么写入Mem要放在Mem阶段:
-
Wr: Nop
-
J-type
jal r, imm20
- Ifetch: 取指令并计算PC+4 (写入PC ,但后续肯定需要修改PC )
- Reg/Dec:从寄存器取数,同时指令在译码器进行译码
- Exec: 执行阶段ALU中计算PC+4(准备写入rd)/Adder中计算转移地址(PC+SEXT(imm20)<<1)
- Mem:把转移地址写入PC
- Wr: 把ALU运算结果(PC+4)写入rd
五阶段流水线数据通路¶
-
红色流水段寄存器:保存每个周期执行的结果,属于内部寄存器,对程序员透明,无需作为现场保存!
- 使用处于的两个阶段命名
-
以load为例,介绍指令的具体执行过程
- 取指令IF
- 流水段寄存器IF/ID存储PC和IR
- 开始时和过程中
- 不需控制信号,因为每条指令执行功能一样,是确定的,无需根据指令的不同来控制执行不同的操作!
- 执行结束时
- 流水段寄存器用来存放各阶段的执行结果总是在下个时钟到来后的Clock-to-Q更新,保存后面阶段用到的指令和旧PC的值!
- 译码/取数(Reg/Dec)阶段
- 流水段寄存器ID/EX存储寄存器读取结果 R[Rs1]、R[Rs2],目的寄存器编号 Rd,IMM 和 PC
- 需要使用 ExtOP信号
- Exec计算阶段
- 流水段寄存器EX/M存储 PC 跳转地址 (PC+IMM)、zero 标志、运算结果、R[Rs2](busB)、Rd
- 控制信号:ALUASrc,ALUBSrc,ALUctr****
- Mem存储器读
-
- 流水段寄存器M/WB存储存储器读取结果,ALU运算结果,Rd
- 控制信号:Branch,jump,memwr
-
B指令(冒险)
-
WB回写(冒险)
-
- 传递数据和Rd
- 控制信号:MemtoReg
-
PC、流水段寄存器不需要写使能,因为每个时钟周期都会发生变化
控制信号¶
-
控制信号都是在 ID 阶段解析指令得到的,之后应用在不同阶段,因此控制信号也要保存在流水段寄存器中。
- 每条指令的控制信号在指令执行期间保持不变
- 控制信号的生成与单周期一致
-
再指令开始执行之前,所有段寄存器都要初始化为 0
反向数据流¶
-
同一周期寄存器有读和写:出现反向数据流利用时钟上升和下降沿两次触发,能做到前半周期写,后半周期读
-
MEM
延迟问题¶
-
流水线执行时,前面指令执行完成之前后面指令就开始执行,由于反向数据流,可能执行的是错误的,如 load 完成之前完成之前就取数,beq 跳转之前就从原先的位置继续执行
-
load 引起的延迟
- 直到第 6 周期才开始能读取到 load 的数据,也就是说如果 234 指令要用都会是错的
- 称为数据冒险(数据相关)
-
beq/jmp 引起的延迟
- 直到第 8 周期周期才转移到目标 PC 开始执行
- 在这之前取错三条指令
- 称为控制冒险(分支冒险、转移冒险)
流水线冒险及其处理 #难点¶
结构冒险¶
- 同一个部件同时被不同指令所使用
- 解决方案:填充空阶段,使得所有指令阶段数目一样,并且执行顺序也一致
- 规定流水线数据通路中功能部件的设置原则为: 每个部件在特定的阶段被用!
- 对于可能同时使用的元件改造,如寄存器上半周期写,下半周期读
- 内存中指令和数据划分区域,使得互不干扰
数据冒险¶
- 后面指令用到前面指令结果数据时,前面指令的结果还没产生
-
- 第 234 条指令使用未经过指令 r1 的旧值
- 称 add 和 sub 发生了数据冒险(add 和 and 发生了数据冒险...)
-
分类
- RAW 写后读(基本流水线中经常发生,如上例)
- WAR 读后写(基本流水线中不会发生,乱序时会发生)
- WAW 写后写 (基本流水线中不会发生,乱序时会发生)
解决方法¶
-
硬件阻塞
- 硬件上通过阻塞方式阻止后续指令执行,延迟到有新值以后。
- 缺点:控制比较复杂,需要改数据通路;指令被延迟三个时钟执行。
-
软件插入 NOP 指令
- 由编译器插入三条 NOP 指令,浪费三条指令的空间和时间。
- 好处:数据通路简单,即无需改数据通路。
-
合理实现寄存器堆的读/写操作(不能解决所有数据冒险)
- 前半写,后半读,可以解决一条冲突
-
转发技术(不能解决所有数据冒险)
- 数据计算出来要早于存储,如何在计算出来之后尽快使用结果,把数据从流水段寄存器中直接取到 ALU 的输入端
-
- 如图所示的三种转发情况(蓝色为 load,并且对于load 之后之后立即使用的情况不能通过转发直接解决,因为此时要 load 的的数据还没有读取出来 )
- 对于这种 load 之后立即使用的情况无法解决,必须延迟执行一条指令,称为:装入- 使用数据冒险(load- use)
- 当前面指令为 load,并且前面指令的目的寄存器是当前的源寄存器(ID/EX. MemRead&&ID/EX. Rd==IF/ID. Rs1||ID/EX. Rd==IF/ID.Rs 2)
- 在阻塞点,将 load 后面两条指令的执行结果清除,并延迟一个周期执行
- 将 ID/EX 段寄存器中所有控制信号清0 ,以插入一个“气泡”
- IF/ID 寄存器中的信息不变(还是 sub 指令),sub 指令将重新译码执行
- PC中的值不变
-
- 蓝色使用前半后半解决,绿色使用转发解决,红色使用 loaduse 解决
-
带“转发”和“阻塞”检测的流水线数据通路
-
编译优化:调整指令顺序(不能解决所有数据冒险)
控制冒险¶
- 转移或异常改变执行流程,后继指令在目标地址产生前已被取出
- 异常中断也是特殊的控制冒险
- 错误取出了 3 条指令,即延迟损失时间片 C=3 (发生转移时给流水线带来的延迟损失)
解决方法¶
- 硬件上阻塞(stall)分支指令后三条指令的执行
- ¶
-
软件上(编译器)插入三条“NOP”指令
-
分支预测
- 预测失败时,需把流水线中三条错误预测指令(C=3)丢弃掉,将被丢弃指令的控制信号值或指令设置为 0 (IF ID EX )
- 流水线控制必须确保被错误预测指令的执行结果不能生效,而且要能从正确的分支地址处重新启动流水线工作
- 简单(静态)预测:
- 总是预测条件不满足,即:不跳转(也可以使用一些简单的启发式规则)
- 预测的正确率与分布有关
- 预测错误的代价与何时能确定是否转移有关。越早确定代价越少(C=1), 最早可以提前到 ID,需要使用额外的电路直接比较 Rs1 和 Rs2 的值进行逻辑运算。
- 预测失败时:
- 将转移目标地址->PC
- 清除IF段中取出的指令,即:将IF/ID中的指令字清0,转变为nop指令
-
动态预测:
- 利用最近转移发生的情况,来预测下一次可能转移还是不转移
- 转移发生的历史情况记录在 BHT(分支历史记录表)中
-
- 首先从 BHT 表中寻找,看这条分支指令以前是否执行过,如果没有则插入新的表项到 BHT (默认设置为顺序取),如果找到了则直接使用预测表中的转移目标地址进行转移(预测发生时,选择“转移取“;预测不发生时,选择“顺序取”)
- 每个表项由分支指令地址低位作索引,故在 IF 阶段就可以取到预测位
-
一位预测位:
- 只根据上次的实际发生情况进行预测
- 连续变化时会造成连续错误
-
两位预测位:(现在通常使用两位或两位以上的预测位)
- 用2位组合四种情况来表示预测和实际转移情况
- 只有连续 两次预测错误才改变预测方向
-
延迟分支(编译优化)
- 属于静态调度技术,由编译程序重排指令顺序来实现
- 把分支指令前面的与分支指令无关的指令调到分支指令后面执行,以填充延迟时间片(也称分支延迟槽 Branch Delay slot),不够时用 nop 操作填充(也就是说无论是否跳转,这段指令都可以执行,不需要回退)
- 使用了延迟分支,就必须保证已经完全消除了控制冒险
-
Jump 指令总是需要两个时钟周期(也就是第一个阶段读取,第二个阶段得到跳转地址,也就是会造成一个时钟周期的延时),之所以不能在第一个周期做,是因为从内存中读取数据已经很慢了
难点¶
三种处理器实现方式对比¶
- 主要功能操作时间:存储单元 200 ps;ALU 和加法器 100 ps; 寄存器堆读写 50 ps.
- 假设 MUX、控制单元、PC、传输线路都没有延迟
- 指令组成:25%取数、10%存数、52%ALU、11%分支、2%跳转
单周期¶
- 最长的为 600ps,因此 N 条指令执行时间为 \(600N\)
多周期¶
- 指令时钟周期数:取数-5,存数-4,ALU-4,分支-3,跳转-3
- 平均 CPU 时钟周期数目:\(5*25\%+4*10\%+4*52\%+3*11\%+3*2\%=4.12\)
- N 条指令执行时间为 \(4.12*200N=824 N\)
流水线¶
- 5 阶段划分
- 假定没有结构冒险,数据冒险采用转发处理(假设 loaduse 出现概率为 50%),分支延迟槽为1,预测准确率为75%;无条件跳转指令的更新地址工作也在 ID 段完成。不考虑流水段寄存器延时,不考虑异常、中断和访存缺失引起的流水线冒险。
- Load 指令平均执行为 1.5 时钟(有一半几率发生 load-use)
- store、alu 一个时钟
- branch 指令:预测成功时一个周期、预测失败时两个时钟 \(0.75*1+0.25*2=1.25\)
- jump 指令:2 个时钟(总要等到译码阶段结束才能得到转移地址)
- 平均 CPI \(1.5*25\%+1*10\%+1*52\%+1.25*11\%+2*2\%=1.17\)
- N 条指令执行时间为 \(1.17*200*N=234N\)