跳转至

4 The Processor

文本统计:约 4327 个字 • 18 行代码

Datapath: Elements that process data and addresses in the CPU, like Registers, ALUs, mux’s, memories, …

4.1 Instruction execution in RISC-V

Fetch :

  • Take instructions from the instruction memory

  • Modify PC to point to the next instruction

Instruction decoding & Read Operand:

  • Will be translated into machine control command

  • Reading Register Operands, whether or not to use

Executive Control:

  • Control the implementation of the corresponding ALU operation

Memory access:

  • Write or Read data from memory

  • Only ld/sd

Write results to register:

  • If it is R-type instructions, ALU results are written to rd

  • If it is I-type instructions, memory data are written to rd

Modify PC

  • for branch instructions

4.2 Detailed description of each step

4.2.1 Instruction Fetch

把输入指令地址 (PC) 从Instruction memory中取出Instruction

4.2.2 R-format Instructions

指令过来直接接到Register的口上,(因为rs1rs2rd在指令的位置是固定的)

  • Read register 1和Read register 2分别为rs1rs2的地址,然后读取出来给到Read data 1和Read data 2输出
  • Write register 为rd,Write Data来自外部输入,然后根据RegWrite来决定是否要写入寄存器

写的控制信号RegWrite

Regwrite的作用在于我们实际上无论是不是写,我们Write register都是要读取的,我们需要一个控制信号来决定,实际上所有的写操作都要这么一个控制信号(数据一直再给)

4.2.3 Load/Store Instructions

Read register operands,读寄存器中存储的基地址

Calculate address using 12-bit offset: Use ALU, but sign-extend offset 计算偏移量,得到真正的地址

Load: Read memory and update register 访问内存并(更新寄存器)

Store: Write register value to memory 或者写入数据

Address读入地址,Readdata输出数据,Writedata写入数据

MemRead:内存读取的时候会有一些问题,避免在不该访问的时候访问

MemWrite:与regwrite同样的道理

Immediate generation: 32位选取立即数,然后扩展给ALU

4.2.4 Branch Instructions

Read register operands 从寄存器中读数据

Compare operands 通过ALU比较二者大小并通过检验是否为0给出输出信号

  • Use ALU, subtract and check Zero output

Calculate target address 计算跳转目标地址

  • Sign-extend displacement 将指令中的立即数拿出来给到ImmGen然后进行位扩展

  • Shift left 1 place (halfword displacement) 左移移位

  • Add to PC value 加到PC值上

根据控制信号以及检测是否为0的信号进行PC是否跳转的选择

Shift left 1可以省略吗?

Shift left 1要画(展示功能),但是实际上可以直接在电路中改变连线以达到同样的效果。

4.3 ?- type Instruction & Data stream

4.3.1 R-type Instruction & Data stream

4.3.2 I-type Instruction & Data stream

I-type Instruction 既可以完成立即数的运算比如addi等等,还可以执行从内存中加载数据的功能ld,选择数据使用MUX即可。

4.3.3 S-type Instruction & Data stream

执行sd存储的指令

先将rs1与立即数计算得到内存中的地址,然后将数据rs2写入相应的数据

4.3.4 SB-type Instruction & Data stream

执行有条件跳转指令:beq等等

rs1rs2的值给到ALU计算后验证是否为0,然后与opcode给出的branch指令求与,给到是否跳转的指令

为什么要将zero与branch求与?

因为一般情况下,ALU也有可能得出结果0,所以需要一个branch,两个都为1时,才执行跳转

关于PC的部分,将立即数与PC相加,然后根据是否跳转来选择相应的PC值。

4.3.5 J-type Instruction & Data stream

执行无条件跳转指令

4.4 Building Controller

There are 7+4 signals

”7” 代表的是 RegWrite ALUSrc MemRead MemWrite Branch MemtoReg jump

“4” 代表的是 ALU operation的四位

ALUScr用来决定是两个寄存器中的数相加还是与立即数相加

Branch用来就决定PC是否完成有条件跳转,Jump用来决定PC是否完成无条件跳转

MemtoReg有两位

  • 00代表在进行R型指令的时候把ALU的结果写入寄存器
  • 01代表在进行S型指令的时候把内存中的数据写入寄存器
  • 10代表在进行J型指令或者SB型指令时候将PC+4写入X1寄存器

剩下三个为MemRead MemWrite RegWrite就是分别控制内存与寄存器的读写操作

4.4.1 Scheme of Controller

ALU operation 是二级译码得到的

4.4.2 Designing the Main Control Unit——First level

Instruction opcode 译码成ALU op MUX R/W然后将ALU op给到ALU Decoder 结合function7 function3进行二次译码

上表需要注意一些X的信号,哪些信号在哪些指令下是无所谓的

4.4.3 Designing the Main Control Unit——Second level

ALU operation is decided by 2-bit ALUOp derived from opcode, and funct7 & funct3 fields of the instruction

根据ALU op funct7 func 3得到相应的ALU control

4.5 Pipelining Design

  • 100 ps for register read or write

  • 200 ps for other stages

但是注意到我们在运行load指令的时候,调用后面的物理部件的时候,前面的物理部件空着,下一条指令没法使用,造成了资源的浪费。

于是我们可以引入流水线的思想,将执行过程重叠

4.5.1 RISC-V Pipeline

我们把RISC-V的每条指令分为五级,每个时钟执行其中的一级指令

  • IF: Instruction fetch from memory 从指令内存中把指令取出

  • ID: Instruction decode & register read 指令的解码以及寄存器读取

  • EX: Execute operation or calculate address 计算操作以及计算地址

  • MEM: Access memory operand 读取内存中的内容

  • WB: Write result back to register 写回内容到寄存器

单周期CPU的CPI为1:每个周期执行一条指令

采用流水线改进后,CPI约等于1(前面和后面会少一些)

为什么CPI改进后变大了还要改呢?

CPU time不仅仅只受CPI影响,还受到Cycles per Instruction的影响,改进后Cycles per Instruction 变成了原来的1/5, 所以流水线加速了CPU time

从这张图可以直观地回答上述问题

为什么有些reg写操作在前读操作在后

分开来是因为避免同时出现对register的读写操作,写在前读在后可以减少Data hazard造成的空缺时间,这样连续的读写信号可以在同一个时钟周期完成,刚好前一个操作完成写操作后一个操作就可以完成读操作。

当所有阶段都是平衡的时候,

\[ \text{Time between instructions}_{\text{pipelined}} = \text{Time between instructions}_{\text{nonpipelined}}/\text{Number of stages} \]

当然我们不可能做到全部阶段都平衡,实际上速度会稍慢一些

流水线未提高latency(time for each instruction), 但是提高了throughput (吞吐量)

4.5.2 Hazard 风险

1 Structure hazards

不同指令之间用的硬件冲突了,但是设计的时候已经将每个指令分为五级操作,所以这个问题不会发生。

2 Data hazard

详细内容见4.7节

Need to wait for previous instruction to complete its data read/write,后一条指令对数据的处理依赖于前一条指令完成后的数据

比如说下面这个例子:

如何解决这个问题,有一个方法:Forwarding (aka Bypassing)

我们可以在两个R型指令之间添加一根线,让上一条指令的EX步骤结束后直接将计算结果给到下一条指令的EX(在存入寄存器之前),这样我们就不需要中间的bubble步骤了

Load use data hazard: 但是对于L型指令与R型指令之间,由于ld指令必须要将数据从内存中取出,必须要在MEM结束后才能执行R指令,所以中间不可避免的会有一层bubble。

我们可以改变指令的顺序,让I型指令和R型指令不要连在一起,来避免这种情况发生,下面就是一个例子

3 Control hazard

Fetching next instruction depends on branch outcome,选取的指令受到前面branch的结果影响

branch再怎么优化都会有一级的停顿,因为下一条指令的产生必须要在ID之后。我们可以优化电路将计算部分往前提,让其在ID后出结果

Branch Prediction

一种提升分支阻塞效率的方法是预测分支不发生并持续执行顺序指令流。一旦条件分支发生,已经被读取和译码的指令就会被遗弃,流水线继续从分支目标处开始执行。如果条件分支不发生的概率是50%,同时丢弃指令的代价又很小,那么这种优化方法就会减少一半由控制冒险带来的代价。

More-Realistic Branch Prediction

  • Static branch prediction

基于典型的分支行为,比如说做循环的时候一般都是分支跳转的,所以我们默认预测是跳转回去 (Predict backward branched taken)

  • Dynamic branch prediction

保存每个条件分支是否发生分支的历史记录,然后根据最近的过去行为来预测未来

4.6 RISC-V Pipelined Datapath and Control

我们会注意到执行WB写入寄存器的地址Write register已经发生了变化,以及将PC与立即数相加的时候我希望加的是原来指令的PC但是此时PC已经发生了改变。为了解决这些问题,我们引入流水线寄存器 (Pipeline register),用于存储上个阶段得到的数据。

Single-clock-cycle and Multi-clock-cycle

1."Single-clock-cycle" pipeline diagram

  • shows pipeline usage in a single cycle
  • Highlight resourced used

指的就是展示一个时钟周期的情况

2."multi-clock-cycle" pipeline diagram

  • graph of operation overtime

展示多个周期的情况

可以这样表示

也可以这样表示

我们以 “single-clock-cycle” diagrams 来看load指令

执行IF阶段

执行ID阶段

执行EX阶段

执行MEM阶段

执行WB阶段

我们注意到在执行WB阶段的时候写回寄存器的地址并不是ld指令的地址,而是另一条指令Write register部分,这样会造成数据写入错误,所以我们应该将ld指令的Write register信息一直保存在流水线寄存器中并在执行WB阶段的时候使用对应寄存器中的内容。

正确的电路如下图所示

对于相应的控制信号,我们也要将其存储到相应的流水线寄存器中

完整的展现出来就是

4.7 Data Hazards

4.7.1 Detect forwarding

sub x2, x1,x3
and x12,x2,x5
or x13,x6,x2
add x14,x2,x2
sd x15,100(x2)

第一条指令会对and or 指令产生影响(注意没有对add产生影响),于是我们需要设计forwarding以及判断什么时候需要forwarding

我们需要按照流水线记录寄存器的编号,比如

ID/EX.RegisterRs1=register number for Rs1 sitting in ID/EX pipeline register

这样的话,ALU的操作数寄存器的编号就是ID/EX.RegisterRs1ID/EX.RegisterRs2

那么我们可以认为 Data Hazards 发生在

//Fwd from EX/MEM pipeline reg
1a. EX/MEM.RegisterRd = ID/EX.RegisterRs1
1b. EX/MEM.RegisterRd = ID/EX.RegisterRs2
//Fwd fromMEM/WBpipeline reg
2a. MEM/WB.RegisterRd = ID/EX.RegisterRs1
2b. MEM/WB.RegisterRd = ID/EX.RegisterRs2

当然我们还需要满足相应的控制信号为真

EX/MEM.RegWrite, MEM/WB.RegWrite

同时Rd寄存器的编号不为0

EX/MEM.RegisterRd ≠ 0,MEM/WB.RegisterRd ≠ 0

相应的电路图

相应的控制信号(每个MUX中输入分别是正常输入,一级前递,二级前递)(后面两个名字是自己取的。。。)

4.7.2 Double Data Hazard

add x1,x1,x2
add x1,x1,x3
add x1,x1,x4

像上面这种情况,既发生一级前递,又发生二级前递的情况,我们称之为 Double Data Hazard。

遇到这种情况,我们应该使用最近的那次前递得到的结果。于是二级前递仅仅只在一级前递不发生的情况下发生

从而MEM hazard(被我成为二级前递)的条件修改为

//针对rs1
if (【MEM/WB.RegWrite and (MEM/WB.RegisterRd ≠ 0)】二级前递的先决条件
and not【(EX/MEM.RegWrite and (EX/MEM.RegisterRd ≠ 0)and (EX/MEM.RegisterRd = ID/EX.RegisterRs1)】不发生一级前递)
and (MEM/WB.RegisterRd = ID/EX.RegisterRs1)) ForwardA = 01
//针对rs2
if (MEM/WB.RegWrite
and (MEM/WB.RegisterRd ≠ 0)
and not(EX/MEM.RegWrite and (EX/MEM.RegisterRd ≠ 0)
and (EX/MEM.RegisterRd = ID/EX.RegisterRs2))
and (MEM/WB.RegisterRd = ID/EX.RegisterRs2)) ForwardB = 01 

修改相应的数据通路

4.7.3 Load-Use Hazard Detection

Load-Use Hazard 发生在这条指令的寄存器操作数是前一条指令是ld从内存中WriteRegister的内容。

我们可以提前检测,这时ALU的寄存器操作数就是IF/ID.RegisterRs1, IF/ID.RegisterRs2

从而Load-Use Hazard发生的条件是

ID/EX.MemRead and
((ID/EX.RegisterRd = IF/ID.RegisterRs1) or
(ID/EX.RegisterRd = IF/ID.RegisterRs2))

如果检测到,那么需要停顿并加一个bubble。

如何添加一个bubble

  • 将ID/EX中的控制信号全部置0
  • 禁止PC寄存器和IF/ID流水线寄存器的改变

总的来说就是,我们先读入and指令,然后进行检测(IF/ID与ID/EX之间的检测),发现不对需要添加bubble,禁止PC寄存器改变与IF/ID寄存器改变,同时置ID/EX的控制信号全部为0。在下一个时钟的时候,读入的指令依然是and,此时我们就可以正常继续了。

相应的数据通路

4.8 Branch Hazards

如果是否跳转的信号在MEM阶段才得出,那么如果我们判断错误,我们就需要刷新三个指令

刷新3个指令只需要将各级的控制信号清零,WB放在最后的好处在于这三条指令产生的结果并未对数据产生影响。

当然这样错误的开销是巨大的,所以我们改变硬件,将相应的结果提前到ID阶段输出,

  • 算目标地址

  • 比较相应寄存器中的值(按位异或之后检测是否为0)【注意这里也可能会有潜在的数据冒险】

移到ID阶段后,这样我们刷新的时候只需要停一个周期

Dynamic Branch Prediction

In deeper and superscalar pipelines, branch penalty is more significant

Use dynamic prediction

  • Branch prediction buffer (aka branch history table) 存储的是上一次是否跳转

  • Indexed by recent branch instruction addresses (lower part)

  • Stores outcome (taken/not taken)

解码出的跳转指令,然后我去检测上一次相同的跳转跳了还是没跳

1-Bit Predictor: Shortcoming

对于一个嵌套循环

outer: ...
       ...
inner: ...
       ...
       【beq ...,..., inner】
       ...
       beq ...,..., outer

对于【】中的跳转语句,如果这次跳转语句错误了(应该不跳转但是跳转了),那么执行下一个跳转语句的时候应该是继续跳转,但是由于上次是不跳转的,那么这次还不跳转,所以又错误了,那么就会造成连续的两次错误。

2-Bit Predictor(有一个缓存的机制)

Only change prediction on two successive mispredictions

Warning

我们这里没有说明两种predictor的优劣,上述的shortcoming是一种2-bit更好的情况

Even with predictor, still need to calculate the target address 我们仍需要计算目标地址,同时也会又一个时钟的延误

我们可以引入 Branch target buffer (BTB),之前那个是BPB存储是否跳转,BTB则是直接存储上次跳转的地址,这样我们下次就不用算了。但是这样会造成较大的空间损耗(一条PC值64位)所以实际上我们需要在两者之间找到相应的平衡

4.9 Exceptions and Interrupts

“Unexpected” events requiring change in flow of control

  • Exception: Arises within the CPU
  • Interrupt: From an external I/O controller

Handling Exceptions

(RISC-V) 一种解决方式是记录下出错的PC值,出错的原因,然后跳转到一个固定的PC值去处理。

Save PC of offending (or interrupted) instruction. In RISC-V, we use Supervisor Exception Program Counter (SEPC)

Save indication of the problem. In RISC-V, we use Supervisor Exception Cause Register (SCAUSE)

这个SCAUSE一共64位,但是很多位是没有被使用的

  • Exception code field: 2 for undefined opcode, 12 for hardware malfunction, …

Jump to handler, assume at \(0000\ 0000\ 1C09\ 0000_{hex}\)

另一种解决方式是根据不同的出错方式跳转到不同的 Handler address,被称为使用向量式中断 (vectored interrupt)。用基址寄存器加上例外原因(作为偏移)作为目标地址来完成控制流转换。

基址寄存器中保存了向量式中断内存区域的起始地址。比如我们可以根据例外类型定义两类例外的例外向量起始地址

例外类型 例外向量地址
未定义指令 \(00\ 0100\ 0000_2\)
系统错误(硬件故障) \(01\ 1000\ 0000_2\)

操作系统可以根据例外向量起始地址来确定例外原因

Exceptions in a Pipeline

——another form of control hazard

Consider malfunction on add in EX stage

add x1, x2, x1

  • Prevent x1 from being clobbered
  • Complete previous instructions 完成之前的操作
  • Flush add and subsequent instructions 对之后的操作进行刷新
  • Set SEPC and SCAUSE register values
  • Transfer control to handler 转移到handler中去处理

与 mispredicted branch的过程类似

Multiple Exceptions

一条流水线可能在同一个周期出现多个exception,我们只需要找到出现exception最早的那一条指令,然后处理即可

Imprecise Exceptions

在流水线处理器中,将例外和指令正确关联起来存在一定难度,一些处理器设计者在非关键情况下考虑放松对此的要求。这样的处理机制被称为非精确中断 (imprecise interrupts) 或者非精确例外 (imprecise exception)。在上文的例子中,当流水线检测到例外时程序计数器PC已经变为\(58_{16}\),而发生例外的是地址为\(4C_{16}\)的add指令。具有非精确例外的处理器将在SEPC寄存器中保存\(58_{16}\),让操作系统去判断到底是哪条指令引发了例外。与大多数处理器相同,RISC-V支持精确中断或精确例外。一个原因是,当流水线不断加深时,非精确例外会增加操作系统处理的难度和复杂度。


后面的内容都不考了,我也就上课听了一下,笔记就没有整理了。。。

评论区

对你有帮助的话请给我个赞和 star => GitHub stars
欢迎跟我探讨!!!