CS:APP Chapter 4 Y86-64处理器设计-读书笔记

4 处理器体系结构

第四章的目标是设计一个 Y86-64 的处理器,并运行设计好的 Y86-64 的指令集。

什么是指令集

指令集 ISA,也就是处理器可以处理的指令的集合,Y86-64 的指令是简化版的 X86-64 指令,他把许多指令都细化了,例如 movq 拆分成了多个 irmovq,rrmovq 等等,直接在指令中写清楚两个操作数的来源以及他们的转移方向。

简化的指令集也让处理器的设计更加简洁和方便,本章主要是设计顺序处理器与流水线处理器,并不涉及乱序处理器的设计。而顺序处理器也分为两个版本SEQ初始版本以及SEQ+版本,流水线处理器分为PIPE-PIPE版本,也就是说一共有四种不同的处理器版本。

什么是顺序处理器

所有指令都是串行执行的,执行完上一条指令然后再执行下一条。

优点是设计比较简单,不需要考虑数据冒险,控制冒险之类的问题。

缺点是对处理器的利用效率比较低下,因为一个指令分为多个阶段,由于是顺序执行,导致会让一部分的阶段处理硬件空闲,所以可以进行一些改进。

什么是流水线处理器

这里我们设计的流水线处理器是将顺序执行的指令处理阶段拆分成多个不同阶段,使得处理器在同一时间内,不同阶段可以处理不同的指令,让所有硬件都得到充分的利用。

优点是利用效率比较高,指令执行速度更快,吞吐量更大。

缺点是设计比较复杂,会有很多的异常情况需要处理,需要处理数据冒险,控制冒险,组合冒险等等情况。

前置知识与指令集设计

涉及到 HCL 硬件控制语言的定义,逻辑门,组合逻辑电路,时钟寄存器等粗浅的硬件知识,还要有对处理器整体的认知。我们使用组合逻辑电路,时钟寄存器,随机访问存储来组成一个最简单的 CPU。

有了对硬件的初步理解之后,再来设计 Y86-64 汇编代码到机器码的指令集。

Y86-64 的指令由 10 个字节组成,第一个字节代表指令类型,也就是icode+ifun的组合,第二个字节rA+rB是源操作数与目标操作数的寄存器代码,寄存器代码就是一个数组的下标,整个寄存器组组成了寄存器文件,相当于我们使用寄存器代码去访问寄存器文件得到寄存器文件中对应下标的数据。不过也不是所有的指令都有这个字节,例如retjXX两个指令就不需要寄存器,对于剩下的 8 个字节或者 9 个字节,进入处理器处理的时候就成了valC,所以在顺序处理器中更新 PC 的时候有些指令要再加上 10 。


设计 SEQ 处理器

实际上 SEQ 也分了很多的阶段,这样做的目的是用尽可能少的硬件去执行尽可能多的不同的指令。

SEQ 的硬件设计中包括了这些:

  • 随机访问存储,其中就包含了数据内存与指令内存,他们分属不同的区域,但确实都在 RAM 中,甚至有些指令可以更改指令内存。

  • PC 增加器,用于计算下一条指令的 PC

  • 寄存器文件,通过接受外部输入的寄存器名称和值来读写寄存器。

  • ALU 也就是算法逻辑单元,用于计算数值,地址,计算状态码,跳转状态等等。

这些简单的硬件就组成了我们的 SEQ 处理器,所有的指令都在一个时钟周期中完成,并且数据流动是自底向上的,而数据反馈写入是自顶向下的。

为了让指令更加统一,我们将它们分为了六个阶段。

取指 Fetch

将程序计数器寄存器作为地址,指令内存读取指令的字节,PC 增加器计算 valP 也就是下一条指令的地址。

译码 Decode

寄存器文件有两个读端口 A 和 B,从这两个端口中同时读取寄存器的值 valA 和 valB,传入的是 srcA,srcB 这两个读取地址一般是来自从指令中解析出来的 rA 和 rB,但有的时候并不需要读取两个值,只需要一个就可以,所以在这种情况下,另一个空闲的读取端口就会被设置为 15,这也就是一开始设计寄存器的时候只设计了 15 个但却保留了 r15 而不使用的原因。

执行 Execute

在这一阶段会根据指令的类型,将算数 / 逻辑单元 ALU 用于不同的目的,对于整数加减之类的操作,它会执行指令指定的运算,而对于其他的指令,ALU 作为加法器来增加或减少栈指针,计算有效的内存地址,或是不对操作数进行改变,仅仅对它加个 0(为了满足统一的加法格式而且不改变操作数的值),将输入传递到输出。

同时也会在这一阶段根据 ALU 计算得到的结果来设置条件码寄存器的值,然后可以计算得到分支跳转信号 Cnd(如果需要跳转的话)。

访存 Memory

通过上一阶段得到的计算结果,或者是直接用指令中解析出来的 valC 地址,访问内存,读出或者写入一个内存字,指令内存和数据内存访问的是相同的位置,但是用于不同的目的。

写回 Write Back

这一阶段的写回指定的是写入寄存器文件,这与之前的译码阶段相对应,在译码阶段只可以进行读取操作,而写入寄存器的操作只可以在这一阶段执行。

寄存器文件有两个写端口,其中端口 E 用来写 ALU 计算出来的值,而端口 M 用来写入从数据内存中读取出的值。这两个端口都是传输要写入的数据!而不是要写入的地址。

PC 更新 Program Counter

程序计数器的下一个值有多个可能,有可能是 PC 增加器(一般指令)计算得到的值,也可能是在当前指令中指定的那个值 valC(对应 jXX 跳转指令),也可以是从内存中读取出来的值 valM(对应 ret 指令)。

而对于考试而言,我看到的一些有关这一章的题目是考的指令分阶段实现。

设计流水线处理器

在流水线化的系统中,待执行的任务被划分成了若干个独立的阶段,这些阶段通常会允许多个任务同时执行,而不是需要等到一个任务完成了所有阶段的任务才会开启下一个任务(这样的处理叫做串行处理,并行流水线处理要比串行处理快得多),不过在这样的流水线系统中,任务难免要经过那 些并不需要的环节,例如上面的 OP 操作就根本没有访存阶段,而它还是要有这个过程,并浪费这么多的时间。

总的来说,流水线化的系统大大提高了系统的吞吐量,也就是单位时间内处理的指令的数量,不过带来的弊端是会轻微增加任务的总体延迟(Latency)也就是处理单条指令的时间。

通过这两张图就能很方便的比较顺序与流水线化的区别。

流水线的局限性

不一致的划分

我们的划分往往是假设每个阶段耗时都是一样的,这样才能充分利用给出的时钟周期,但实际情况中并不那么完美,就像有的指令根本没有访存阶段一样,不可能所有划分出来的阶段耗时都是一样的,所以就会造成浪费,但是如果不按照最长耗时的阶段来给定时钟周期,那么有的阶段就会完不成,导致流水线出错。

流水线过深,收益下降

我们将计算划分成 6 个阶段,每个阶段需要 50ps,再在每对阶段之间插入流水线寄存器,我们就得到了新的六阶段的流水线,这个系统的最小时钟周期达到了 70ps,吞吐量为 14.29GIS,吞吐量也就是最小时钟周期的倒数。

虽然我们把三阶段的流水线提升为六阶段的流水线,但是我们的吞吐量并没有翻倍,是中间插入的寄存器,也就是流水线延迟过多,导致延迟在整个时钟周期的占比提高(20/70=0.286),最后的吞吐量没有得到两倍提升。

流水线阶段及流程设计

这一阶段的流水线设计增加了流水线寄存器(F,D,E,M,W),然后将 PC 的更新移动到了取指阶段,也就是在开始的时候计算当前要执行的指令的地址。

反馈与冒险

反馈是一种依赖,是后执行的指令依赖先前执行指令的结果,因为代码是人写的,人的惯性思维往往是线性的,后来者依赖先行者是理所当然的事情,但由于流水线的划分和流水线并发的特性,导致后执行的指令在译码阶段等读取数据的时候往往前面的指令还没有完成写回操作去更改寄存器文件,也就出现了冒险。

控制冒险

  • 结果没有被及时的反馈给下一个操作。

  • 流水线改变了系统的行为。

数据冒险

  • 一条指令的结果作为另一条指令的操作数(一般是读后写数据相关)。

  • 我们需要处理这类问题,目标是得到正确的结果,并最小化对流水线性能的影响。

冒险处理手段

  • 添加气泡 bubble

  • 暂停 stalling

  • 数据转发 forward,增加旁路路径来把后续指令需要的数据从前置指令中转发出来,而不需要暂停等待前置指令更新寄存器。

    • 转发源:e_valE, m_valM, M_valE, W_valM, W_valE

    • 转发目的地:val_A, val_B

    • 其中开头大写的是流水线寄存器中的值,小写的是流水线阶段中产生的信号。val_A, val_B 是 ALU 的操作数。

冒险具体类型

加载 / 使用冒险 Load/Use Hazard

检测手段:在执行阶段判断当前执行的指令是不是 mrmovq 或者 popq 指令,以及指令要写入的目标地址是不是译码阶段给的源地址。

解决方法

  • 将指令暂停在取指和译码阶段

  • 在执行阶段的那条指令加入气泡,等待数据加载完成。

分支预测错误 Mispredicted Branch

对于分支预测有很多的策略,例如永远选择 Always Taken,也有永不选择 Never Taken 或是其他的更加复杂的策略,前者的预测正确率大概为 60%,后者为 40% 左右,Y86-64 中使用的是前者,但不管哪个策略都会往下执行两条指令,因为只有分支跳转指令完成执行阶段(Execute)才能算出 Cnd 的准确值,所以肯定有两个新的指令已经加入流水线了,如果那两条指令不是正确的指令,那么就要取消他们的执行,不过根据我们设计的六阶段流水线处理器,他们并不会改变寄存器和状态,所以只要单纯地取消即可。

检测手段

解决方法

  • 在执行阶段检测到未选择该分支

  • 在紧跟着的指令周期中,将处于执行和译码阶段的指令用气泡替换掉,气泡指令实际上就是 nop 指令。

  • 此处不会出现预测错误的副作用,所以不需要接着处理。

ret 指令

因为 ret 指令需要到达写回阶段才算结束,而在它之后执行的三条指令需要暂停,也就是插入气泡,让接下来的三条指令都暂停在取指阶段,前面的指令不受影响,继续正常执行。

检测手段

解决手段

  • 当 ret 经过的时候,接下来的指令都暂停在取指阶段。

  • 在后三条指令的译码阶段插入气泡。

  • 当 ret 指令执行到写回阶段的时候释放暂停。

冒险控制小结

组合情况的处理

在一个时钟周期内多种不同的流水线冒险同时出现

  • 组合 A

    • 不选择分支

    • 位于分支中的 ret 指令

  • 组合 B

    • 指令从内存读取到 % rsp

    • 紧跟着 ret 指令

异常处理

我们需要遵循的异常处理原则是出现异常的指令的后续指令不能改变处理器的状态,所以我们应该禁用对条件码寄存器的修改以及数据内存的修改。

一条指令在流水线的某个阶段产生了异常,那么就应该将异常状态写入到流水线寄存器的状态码中,然后让这个状态码随着流水线传播,直到协会阶段再写入处理器的状态寄存器 Stat。


还有很多像PC的预测,处理器性能分析之类的没有写,不过多是细枝末节的,重要的以后看到了还是要补充。

posted @ 2021-09-22 23:25  tanknee  阅读(457)  评论(0编辑  收藏  举报