Skip to content

4 Processor

约 4622 个字 20 张图片 预计阅读时间 15 分钟

Abstract

计组的这个章节是要完成信电配套的计组实验课lab30必须要学会的部分;
前人的笔记真的写的相当好了,作为后辈俺进行了一些整合,因此有了这份Note。
参考(几乎是照抄)
咸鱼暄前辈的Note
NoughtQ前辈的Note
由于整合后的风格和之前都差的很多了,所以就发在这里而没有提pr
在整合这份笔记的时候还用了Gitzip插件,一并致谢。

Warning

本文档正在完善!!现在还处于抄写阶段QAQ

4.1 DataPath

4.1.1 Overview

Info

这一部分有个大致概念就好,后面会具体再分析。

RISC-V指令集架构下的最最简单的CPU应该要实现以下核心指令:

  • 内存引用指令(memory-reference instructions):ldsd...
  • 算术逻辑指令(arithmetic-logical instructions)/R型指令:addsubandor...
  • 条件分支指令(conditional branch instructions):beq...

下面简单介绍一下如何实现这些指令:

  • 最开始的两步是相同的:
    • 取指(IF):根据PC(程序计数器)值(即当前指令的地址),从指令内存中获取当前周期下需要执行的指令
    • 译码(ID):根据指令的字段识别指令类型,并从单个或多个寄存器内读取数据(load指令只需读取一个寄存器即可),也可以不读取数据(比如jal指令)
  • 后面的步骤则因指令类型而异:
    • 执行(EX):大多数指令都会用到ALU,但是有不同的目的:
      • 内存引用指令:地址计算(利用加法)
      • 算术逻辑指令:执行算术逻辑运算
      • 条件分支指令:判断两数的大小关系,同时计算候选的跳转地址(利用减法)
      • 无条件分支指令:计算跳转地址(但不是在ALU上完成的)
    • 访存(MEM)/写回(WB):ALU完成任务后,后续操作的区别会更大:
      • 内存引用指令:
        • 加载指令:读取数据(访存 + 写回)
        • 存储指令:写下数据(访存)
      • 算术逻辑指令:将ALU的计算结果写入寄存器中(写回)
      • 条件分支指令:既没有访存也没有写回
      • 无条件分支指令:一般情况下,返回地址(PC + 4)会被写入寄存器x1里(写回);若不用返回地址,也可以将返回地址传给x0

注:综上,绝大多数指令只需四步即可完成,只有加载相关的指令需要五步。

这是一张 RISC-V 核心指令的实现图(并不完整),之后我们会用这张图进行分析。

在进入正式的DataPath讲解之前,我们先回顾一下第二章学过的指令及指令格式:

4.1.2 R 型指令

  • add , sub , and , or 这几个 R 型指令总共需要访问 3 个寄存器,如下图所示:

  • (1) 处取出指令, [6:0]funct7)被送到 Control 产生对应的控制信号,我们稍后可以看到; [19:15] , [24:20] , [11:7] 分别对应 rs1 , rs2 , rd ,被连入 Registers 这个结构,对应地 Read data 1Read data 2 两处的值即变为 rs1 , rs2 的值;
  • (2) 处 MUX 在 ALUSrc = 0 的信号作用下选择 Read data 2 作为 ALU 的输入与 Read data 1 进行运算,具体的运算由 ALU control 提供的信号指明(我们在 4.1.3 小节 讨论这个话题)。运算结果在 ALU result 中。

Tip

ALUSrc:决定 ALU 的第 2 个操作数——低电平时 ALU 获取第 2 个寄存器的值,高电平时 ALU 获取立即数

  • (3) 处 MUX 在 MemtoReg = 0 的信号作用下选择 ALU result 作为写回 Register 的值,连到 (4) 处;在 (5) 处 RegWrite = 1 信号控制下,该值写入到 rd 寄存器中。

这就是 R 型指令的运行过程。执行完指令后 PC 会 +4,我们在 4.1.4 小节 讨论这一操作的实现。

Info

有聪明的小朋友可能会问,为什么需要 RegWrite 这个控制信号呢?非常简单,因为 Write registerWrite data 这两条线一直连着对应的端口,但不是每个指令都需要写入寄存器。如果在没必要的时候写入,就篡改原先的寄存器值了。Reg files 需要知道什么时候需要写入寄存器,因此只有当 RegWrite = 1 时才会被对应地写入。

聪明的小朋友可能又会问了,为什么 PC 寄存器也要写入(插播:PC寄存器不是那个大大的存储指令地址的地方!是下条指令的地址,会变的!) ,但是不需要控制信号呢?非常简单,因为 PC每个 时钟周期都会被写入,所以只需要在时钟的 每个 上升沿或者下降沿触发就好了(我们采取的设计是下降沿触发,我们在 4.2.3 小节 再讨论为什么这样设计),不需要额外的控制信号了。

4.1.3 ALU Control

为什么要有ALU Control?

我们之前在第三章里设计的ALU整合了add/subtract/and/or,除了操作数,需要Control信号来告诉ALU到底执行什么指令!

在之前的章节里,我们设计的 ALU 需要这样的控制结构:

|350

我们列一下需要使用 ALU 的指令的表格(我们在):

我们根据这个表列出真值表:

其中可以发现,标为绿色的列的取值要么是 0 要么是无关的,因此它们并不影响结果。

根据这个真值表构造门电路,就可以构造出 ALU control 单元了。如图中所示,该单元依赖 Control 单元给出的 ALUOp 信号以及 I[30, 14:2]

Info

ALU control 模块可以这样实现:

|450

需要理解的是,我们并不是根据机器码来构造电路的,而是相反:电路的设计参考了很多方面的问题,机器码应当主要迎合电路使其设计更加方便。

4.1.4 跳转指令与 Imm Gen 模块

  • 在一条指令运行完后,如果不发生跳转,PC + 4,否则 PC 跳转到 PC + offset 的位置去。这个过程是如何完成的呢?看下图:

|600

跳转分为两部分:判断条件是否成立/跳转到对应语句

4.1.4.1 判断条件是否成立

  • (1) 中有两个加法器,一个的结果是 PC + 4,另一个是 PC + offset,其中 offset 是来自当前 instruction 的;这两个加法器通过 MUX 送给 PC
  • MUX 的控制信号来自 (2), (2) 是一个与门,即当且仅当两个输入信号都为真时才会输出 1,从而在上述 MUX 中选择跳转。 (2) 的两个输入分别来自:
    • (5) 这个 ALU 的 Zero 信号,这是第 3 章中我们设计的可以用来实现 beq 的结构;我们讨论过实现 beq 其实就是计算 rs1 - rs2 判断其是否为 0,所以这里根据 Zero 是否为 0 就能判断两个寄存器是否相等
    • (4) 处 Control 给出的 Branch 信号,即如果这个语句是跳转语句,那么对应的信号会置为 1

也就是说,当且仅当语句确实是 beq 而且 Zero 信号的值确实为 1 时才会进行跳转。

4.1.4.2 跳转到对应语句

  • 再来看看当进行跳转的时候, (3) 处的 offset 来自哪里。我们可以看到,实际上这个 offset 来自于 I[31:0] ,也就是整个指令;它被传给 Imm Gen 模块,将指令中的立即数符号扩展到 64 位;然后在 (3) 处左移 1 位,再与 PC 相加。

SB-Type

回顾一下我们之前学的SB-Type【条件分支(inst rs1, rs2, imm[12:1])】:

| Name | 7 bits | 5 bits | 5 bits | 3 bits | 5 bits | 7 bits |

| ------- | -------------- | ------ | ------ | ------ | --------------- | ------ |
| SB-type | immed[12,10:5] | rs2 | rs1 | funct3 | immed[4:1,11] | opcode |

  • 可以看到,指令里一共有imm[12:1]的立即数字段,缺了个imm[0],这是因为RISC-V 指令总是 2 字节对齐,所以末尾一定是0。那么为了让尽可能多的位表示立即数,因而规定imm[0] = 0。因此立即数能够表示的范围为-4096\~4094(-1000\~0FFE),且都是偶数。
  • 因此,我们在设计这里的跳转的时候,也需要**考虑这一点

Imm Gen 模块

Imm Gen 模块在load 类指令、 store 类指令和 branch 类指令里都要用到。这里我们先看branch类里,这个模块在参与什么事情:

  1. 将指令中的立即数符号扩展到 64 位(为什么?)
为什么符号扩展到64位?
  1. 为什么扩展?
  2. ALU需要位数对齐(很显然,是逐位扔进加法器的)
  3. 为什么符号扩展?
  4. 我们来看看这个立即数后面都会参与什么运算:
    • 算术运算 (addi,slti)
    • 内存访问(base+offset
    • branch(PC+offset
  5. 一看就知道大家都是有符号运算 ac01
  1. 在 (3) 处左移 1 位(为什么?SB-Type的特殊性)
  2. 与 PC 相加。

其中2.3. 都是在图示的(3):Add模块处实现的。

关于Imm Gen模块怎么区分三类指令
  1. 为什么要区分?
    A:这三类指令虽然都包含立即数,但它们在32位指令中的编码格式完全不同

  2. I-type (Load):立即数是连续的12位 I[31:20]

  3. S-type (Store):立即数被拆分成了两部分 I[31:25]I[11:7]
  4. B-type (Branch):立即数被拆分得更零散,分布在 I[31], I[7], I[30:25]I[11:8]
  5. 怎么区分?—— 看I[6:5]
指令类型 例子 格式 Opcode I[6:0] 关键位 I[6:5]
Load ld, lw I-type 0000011 00
Store sd, sw S-type 0100011 10
Branch beq, bne B-type 1100011 11

4.1.5 Load 指令和 Store 指令

最后我们来看Load和Store指令:

4.1.5.1 Load 指令

load 指令的作用是从内存中读取数据,并将其存入一个寄存器。 如:ld rd, offset(rs1)

  • (1) 处取出指令,控制单元(Control)识别出这是一条 load 指令。指令的 [19:15] 字段对应 rs1(基地址寄存器),其地址被送入 Registers 结构,对应的值从 Read data 1 端口读出。
  • 指令中用于表示 offset 的部分被送入 Imm Gen 模块进行符号扩展,生成一个64位的立即数。

  • (2) 处 MUXALUSrc = 1 的控制信号作用下,选择 Imm Gen 扩展后的立即数作为 ALU 的第二个输入。ALU 将 Read data 1rs1 的值)与该立即数(offset)相加,计算出最终要访问的内存地址。运算结果在 ALU result 中。

  • 计算出的地址(ALU result)被送入 Data memoryAddress 端口。在控制信号 MemRead = 1 的作用下,Data memory 从该地址读取数据。

  • (3) 处 MUXMemtoReg = 1 的控制信号作用下,选择从 Data memory 读出的数据(MUX 的输入 1),并准备将其写回寄存器。

  • (4) 处写回,上一步 MUX 选择的数据被送到 RegistersWrite data 端口。指令的 [11:7] 字段(对应目标寄存器 rd地址)被送到 Write register 端口。最后,在 RegWrite = 1 信号作用下,从内存中取出的数据被成功写入目标寄存器 rd 中。

4.1.5.2 Store 指令

store 指令的作用是将一个寄存器中的数据存入内存的特定位置,如:sd rs2, offset(rs1)

  • (1) 处取出指令,控制单元识别出这是一条 store 指令。[19:15] 字段对应 rs1(基地址寄存器),其值从 Read data 1 读出;[24:20] 字段对应 rs2(源数据寄存器),其值从 Read data 2 读出。指令中的 offset 部分同样被 Imm Gen 模块符号扩展到64位。

  • (2) 处 MUXload 指令一样,在 ALUSrc = 1 信号作用下选择 Imm Gen 的输出。ALU 将 Read data 1 的值(rs1)与立即数(offset)相加,计算出要写入的内存地址。运算结果在 ALU result 中。

  • 计算出的地址(ALU result)被送入 Data memoryAddress 端口。与 load 不同的是,rs2 的值(从 Read data 2 读出)送到了 Data memoryWrite data 端口

  • 在控制信号 MemWrite = 1 的作用下,Data memoryWrite data 端口上的数据(即 rs2 的值)写入由 Address 端口指定的内存单元中。

  • 对于 store 指令,它不需要写回任何结果到寄存器堆。因此,控制信号 RegWrite 会被设置为 0,(4) 处的写回操作不会发生,从而保证了没有寄存器被错误地修改。

4.1.6 Control

看完上述若干小节,control 单元的设计也非常显然了。我们很容易给出如下真值表:

后面就是连电路的工作了。连出来长这样:

|300

4.2 Pipeline

4.2.1 Intro

在小学奥数中我们就学过,并行能够提高整体的效率,例如这个洗衣服的例子:

|575

对于单个工作,流水线技术并没有缩短其运行时间;但是由于多个工作可以并行地执行,流水线技术可以更好地压榨资源,使得它们被同时而不是轮流使用,在工作比较多的时候可以增加整体的 吞吐率 throughput,从而减少了完成整个任务的时间。

在本例中,由于流水线开始和结束的时候并没有完全充满,因此吞吐率不及原来的 4 倍(4 来自于例子中有 4 个步骤);但是当工作数足够多的时候,吞吐率就几乎是原来的 4 倍了。

回到 RISC-V 中来,一个指令通常被划分为 5 个阶段:

  1. IF, Inst Fetch,从内存中获取指令
  2. ID, Inst Decode,读取寄存器、指令译码
  3. EX, Execute,计算操作结果和/或地址
  4. MEM, Memory,内存存取(如果需要的话)
  5. WB, Write Back,将结果写回寄存器(如果需要的话)

各阶段会用到的组件如下图所示(这个图还有很多问题,我们后面慢慢讨论~),可以看到这些部分是可以并行执行的(比如 Reg File 可以一边读一边写):

|600

其加速的核心主旨是,它们都希望一个周期能完成一条指令,但是单周期 CPU 的一个周期需要承担一个指令的所有步骤;而流水线技术引入后,由于它可以并行地同时执行五个阶段的步骤,所以此时的周期只需要一个阶段的长度。总的来说,单周期 CPU 的时钟周期由总耗时最长的指令决定,流水线 CPU 的时钟周期由耗时最长的指令阶段(IF, ID 等)决定。

也就是说,我们本来是在一个周期中完成一个指令,而现在是在一个周期中完成五个不同指令的不同阶段。当然,每个时钟周期的长度也需要足够任何一个阶段完成执行。

RISC-V 也有很多流水线友好的设计,例如:

  • 所有 RISC-V 的指令长度相同,这可以方便 IFID 的工作
  • RISC-V 的指令格式比较少,而且源寄存器、目标寄存器的位置相同
  • 只在 load 或 store 指令中操作 data memory 而不会将存取的结果做进一步运算,这样就可以将 MEM 放在比较后面的位置;如果还能对结果做运算则还需要一个额外的阶段,此时流水线的变长并没有什么正面效果

Hazards 指的是阻止下一个指令在下一个时钟周期完成的一些情况。主要分为三种:

  • Structure hazards
    • 一个被请求的资源仍处于忙碌状态(大多数现代处理器基本不存在这样的问题,可以通过设计ISA解决)
  • Data hazards
    • 需要等待上一个指令完成数据读写(本质上是由于指令之间的依赖关系产生的。如果有一条正在执行的指令要将结果写入寄存器文件,那么后面的指令不能依赖于它写入之前的那个操作数。)
  • Control hazards
    • 一些控制取决于上一条指令的结果(在分支指令中比较常见,若下一条指令在分支指令之后,那么如果分支指令被执行进行了跳转,下一条指令可能无效。)

4.2.2 Structure Hazards

① 两条或多条指令需要同时访问同一个物理资源,从而产生竞争

解决方式:
a) 大家轮流使用。意味着一些指令需要暂停(stall),释放之后再用 —— 会导致效率降低
b) (Final) 增加更多硬件资源,一人用一个 (例如在单周期CPU的设计中,取指令地址的PC+4和R-指令的ALU用的不是同一个ALU)

② 不同指令需要读取同一个物理资源,产生冲突

解决方式:
设置两个独立的读端口和一个独立的写端口,可以实现一个时钟周期内最多三次并发访问(两次读,一次写),从而避免了寄存器访问的结构性冒险

③ 当指令获取(Instruction Fetch)和数据访问(如 load/store 指令)需要同时访问内存时,如果只有一个内存单元,就会产生冲突

解决方式:

  • 使用两个内存:
    • 在处理器芯片上有一些内存块,包含了从主内存复制的指令副本和数据副本,并有两种独立的方式访问他们,就不必争夺主内存。
    • 当我们获取指令(IMEM)的时候,另一条指令可以同时执行读取或者写入的操作

因此,以上问题都可以通过ISA设计和硬件设计解决(以及内存+内存副本)。

4.2.3 Data Hazards

让我们利用【单时钟周期流水线图(single-clock-cycle pipeline diagrams)】来回顾一下之前学的单周期CPU。

适用于Pipeline-流水线的改造:插入寄存器

在取指结束后,插入两个寄存器:
- 一个用于程序计数器(PC)
- 一个用于指令的寄存器(\(\text{inst}_{\text{ID}}\))

因为五级流水线有五条指令在同时执行,所以我们会在下游保存之前获取的四条指令的副本。

每个流水线寄存器都必须保存与在该特定阶段执行的指令相对应的位,因此该阶段需要保存指令+对应的控制位。

另一个设计的不同是:我们实际上在内存访问阶段才执行PC+4,移到下一条命令。

Comments