CPU流水线设计
指令流水线
: 若把指令执行拆分成取指令
-> 指令译码
-> 指令执行
三个部分、这就是一个三级的流水线、若把指令执行进一步拆分成 ALU 计算(指令执行)
-> 内存访问
-> 数据写回
就变成了5级流水线、
五级流水线:
表示在同一个时钟周期里、同时运行5条指令的不同阶段, 虽然执行一条指令的时钟周期变成5、但可以提高CPU的主频, 只需要保证最复杂的一个流水线级的操作在一个时钟周期内完成、无需确保最复杂的那条指令在时钟周期里执行完成
超长流水线性能瓶颈
增加流水线的深度是有性能成本的
用来同步时钟周期的、不再是指令级别, 而是流水线级别、每一个流水线对应的输出、都要放到流水线寄存器(Pipeline Register) 然后在下一个时钟周期交给下一个流水线级处理、so. 增加流水线级、就要多出一级写入流水线寄存器的操作、虽然很快、假设20ps、但不断增加流水线深度、这些操作占整个指令的执行时间的比例就会不断增加
eg. 指令执行3ns(3000ps)、设计20级的流水线、流水线的操作就需要 20ps * 20 = 400ps 占比 400/3000超过10% 、也就是但从的增加流水线级数、会带来更多的额外开销、
所以要合理的设计流水线级数
流水线设计的冒险
流水线设计需要解决的三大冒险: 结构冒险
, 数据冒险
和 控制冒险
结构冒险
: 本质上是硬件层面的资源竞争问题、CPU在同一个时钟周期、同时在运行两条计算机指令的不同阶段、但可能会用到同样的硬件了
如上所示: 在第一条指令执行到访存(MEM)阶段的时候、流水线里的第4条指令、在执行指令Fetch的操作、访存和取指令、都要进行内存数据的读取, 内存只有一个地址译码器的作为地址输入、就只能在一个时钟周期里读取一条数据、无法同时执行第一条指令的读取内存数据和第4条指令的读取指令的操作
类似的资源冲突:
常用的键盘、不是每个键的背后都有一根独立的线路、而是多个键公用一个线路、如果在同一时间、按下两个共用一个线路的按键、这两个按键的信号都没办法传输出去、就出现了按下键却不生效的情况
在CPU的结构冒险里、对于内存访问和取指令的冲突、直观的解决方案是把内存拆成2部分、让他们有各自的地址译码器、分别是存放指令的程序内存和存放数据的数据内存(对应体系结构叫 哈佛架构(Harvard Architecture)), 但这样拆对于指令和数据需要的内存空间、就无法根据实际的应用动态分配了、解决了资源冲突问题、也失去了灵活性
现代CPU架构借鉴了哈佛架构的思路、采用了普林斯顿架构、在高速缓存方面拆分成指令缓存和数据缓存
内存的访问速度远比CPU慢、所以现代的CPU不会直接读取主内存、会从主内存把指令和数据加载到高速缓存中、这样后续访问都是访问高速缓存、而指令缓存和数据缓存的拆分使得CPU在进行数据访问和取指令的时候、不会发生资源冲突了
数据冒险
: 三种不同的依赖关系
数据冒险就是多个指令间有数据依赖的情况、可以分为 先读后写、先写后读和写后再写、如果满足不了依赖关系、最终结果就会出错
通过流水线停顿(Pipeline Stall):插入nop操作 来解决数据冒险
操作数前推
add $t0, $s2,$s1
add $s2, $s1,$t0
1. 第一条指令 将s1 和 s2 寄存器里的数据相加、写入 t0 寄存器
2. 第二条指令 将 s1 和 t0 的数据相加、写入 s2 寄存器
指令2 的执行、依赖t0的值、而t0的值来自于前一条指令的计算结果、所以指令2需要等前一指令的数据写回之后才能执行、就遇到了数据依赖冒险、
使用插入nop来解决、可以解决、但是会浪费两个时钟周期
其实、如果第一条指令的执行结果可以直接传给第二条指令的执行输入、在第一条指令的执行阶段完成之后、直接将数据传输给下一条指令的ALU、下一指令就不需要插入两个nop阶段、就可以继续执行阶段, 如下图所示、这种方案就叫操作数前推
image.png
乱序执行
即使综合运用流水线停顿、操作数前推、增加资源等解决结构冒险和数据冒险的问题、仍然会遇到不得不停下整个流水线等待前一指令完成的情况、但是: 如果后边有指令不需要依赖前边指令的执行结果就可以不必等待前边指令完成、直接占用nop即可、这样的解决方案、在计算机里叫 乱序执行
不得不nop填充.png
nop占用.png
乱序执行的CPU.png
乱序执行的步骤
1. 取指令和指令译码阶段、同其它CPU、会一级一级顺序进行Fetch和Decode
2. 指令译码完成后、CPU不会直接进行指令执行、而是进行指令分发、把指令发到保留站(Reservation Stations)、类似火车站、指令类似列车
3. 指令不会立即执行、而是等待依赖数据完成才执行. 类似火车要等乘客到齐才出发
4. 依赖数据到齐之后、指令可以交给后边的功能单元(Function Unit, FU)、就是ALU来执行、很多功能单元可以并行运行、但不同功能单元可以执行的指令不同、类似铁轨可以从上海北上、到北京或者哈尔滨;有些是南下、到广州或者深圳
5. 指令执行的阶段完成后、不能立即把结果写回寄存器、而是把结果写入指令重排区(Re-Order buffer, ROB).
6. 在重排缓冲区、CPU会按照取指令的顺序、对指令的计算结果重新排序、 只有排在前边的指令都完成才会提交指令、完成整改指令的运算结果
7. 实际的指令计算结果不直接写入内存或者高速缓存、而是先写入存储缓冲区(Store Buffer),最终才写入高速缓存和内存
so. 在乱序执行的情况下、只有CPU内部指令的执行层面、可能乱序、只要在指令的译码阶段正确的分析出指令之间的数据依赖关系、乱序就只会在无相互影响的指令间发生
分支预测
缩短分支延迟: 将调解判断、地址调整 提前到指令译码阶段进行、无需放到指令执行阶段. 这种方式本质上和数据冒险的操作数前推方案类似、就是在硬件电路层面讲一些计算结果更早的反馈到流水线中、反馈更快、后边指令的等待时间就变短了
分支预测: 1) 静态预测、假装分支不发生. 分支预测失败的代价是: 丢弃已取出的指令&清空已使用的寄存器的操作
- 动态分支预测:
a. 一级分支预测: 用1比特、记录当前分支的比较清空、来预测下一次分支时候的比较情况
b. 双模态预测器: 从2byte记录对应状态、提高预测的准确度
CPU的流水线设计里、会遇到eg. 指令依赖等的情况、使得下一条指令不能正确执行. 但是通过抢跑的方式、可以得到提升指令 吞吐率 的机会、流水线架构的CPU、是主动的冒险选择
流水线设计总结
为了不浪费CPU的性能、把指令执行的过程切分成一个个的流水线、来提升CPU的吞吐率, 而每增加一级的流水线、也会增加overhead、同样因为指令不是顺序执行
数据冒险和分支冒险: 通过插入nop来解决
nop空转: 通过乱序执行来解决
乱序执行: 是在指令执行阶段通过一个类似线程池的保留站、让系统之家动态调度先执行哪些指令、前提是不破坏数据依赖性. CPU只要等到在指令结果的最终提交阶段、再通过重排序的方式、确保指令是顺序执行的
超标量: (Superscalar) 和 多发射(Multi issue) 在同一时间把多条指令发射到不同的译码器
网友评论