CSAPP3e第四章[草稿施工中]

CSAPP3e第四章[草稿, 这一篇还在写]

这一章理解难度不大, 但是内容很多, 比较复杂, 你可能读着读着时而觉得”我去这简直是艺术品”时而觉得”我他妈快睡着了”。

指令集体系结构

  • 指令集与指令编码
    pushq/popq二义性(数据操作在外侧, 栈指针变化在内测)
  • 编码寄存器
  • 异常处理

描述体系结构的方法: 硬件设计语言 HCL

简述: 逻辑门->组合电路(==/多路复用器/ALU/集合关系)

通过 AND/OR/XOR 等逻辑门的输入输出相连构成组合电路。一些常见的逻辑电路:

  • 字级别的组合电路: 根据需求把位级的逻辑门串起来即可
  • == 的组合电路实现: 每一位都相等并且 AND 起来
  • 多路复用器: 其实就是 if, 通过 code 的真假来选择输出值是输入值 A 还是输入值 B
  • 集合关系

以及上述组合电路的 HCL 语言描述

时钟信号,组合电路, 存储器

时钟信号每定时间输出高/低信号形成时钟周期,
每个时钟周期:

  1. 先一次时钟上升更新存储器使其输出恒定为组合电路中的新输入
  2. 然后时钟信号下降,组合电路变化

处理器阶段与时序设计

  1. Fetch:
    • M[PC] 取指 icode
    • M[PC+1]rA, rB 的寄存器名字
    • 根据指令长度计算新的 PC 值 valP
  2. Decode
    • rA, rBvalA, valB
  3. Execute
    • 计算 valE = valB(0) OP(+) valA/valC
    • [setCC]
  4. Memory
    • 从内存中读取到 valM, 或写入内存
  5. Write-back
    * 将 valE 写回 rB
  6. Update
    * PC 设置为新 valP

设计顺序执行的 Y86-64 处理器硬件结构: SEQ

用组合电路和存储器描述上述时序即可。
一个比较泛用的方案的简单描述:

1
PC->指令内存->寄存器->ALU->Memory->寄存器->PC

至于具体的 HCL 描述:
数据是自然在组合电路中流动并在时钟上升时”锁”在存储器里的,
HCL 的描述基本上就是根据 icode 或者其他输入值的类型来”选择”输出值
不过要考虑的还挺多的, 拿更新 PC 来说, 不同指令下要更新 PC 的值是不同的,

  • call 就是 call 的参数 valC
  • jmp 并且条件码为真也是 jmp 的参数 valC
  • ret 就是访存获得的之前压栈的返回地址 valM

流水线执行的 PIPE 处理器硬件结构

流水线原理

每条指令从一个大的组合逻辑->寄存器的执行,
改为拆分出的组合逻辑A->寄存器->拆分出的组合逻辑B->寄存器->...
流水线就是将这样的多条指令中的不同阶段并行[1]执行
之所以中间还要加寄存器是由于不同阶段组合逻辑是并行执行的, 所以每个阶段都必须要保存状态

  • 提升: 多条指令的不同阶段可以重叠起来(因为不同阶段用到的硬件不同, 避免 CPU 出现空闲)

  • 降低: 多加了几个寄存器的位置,流水线寄存器操作有一定开销 所以流水线设计是平衡的艺术, 可以用一些值衡量:

  • 吞吐量:

    (1)1 Instructionmax()+×1000ps1ns

    单位是 , 每秒千兆(十亿)条指令

  • 延迟: 一条指令执行所需的时间(ps), 吞吐量的倒数

    [1]: 并行(Parallel)和并发(Concurrency)的区别: 前者是同一时间内同时执行(流水线一个周期内同时处理多个指令的不同阶段, 多执行单元乱序执行即下一章说的超标量, 多核心 CPU 等), 后者是通过快速切换任务使得看起来”在同时运行多个任务”(单核 CPU 上的的多线程和操作系统进程并发(上下文切换), 操作系统中断处理)

    另外从概念定义以及概念的应用上流水线就是并行, 也不是什么”准并行”, 和你说并发的都可以埋了。

    Pipeline (computing) - Wikipedia

    In computing, a pipeline, also known as a data pipeline, is a set of data processing elements connected in series, where the output of one element is the input of the next one. The elements of a pipeline are often executed in parallel or in time-sliced fashion. Some amount of buffer storage is often inserted between elements.

流水线的局限性

  1. 不等长的划分, 吞吐量的分子是按最长时间的组合逻辑计算的(其他并行的组合逻辑需要空闲等待, 造成浪费)
  2. 流水线过深, 拆分的的组合逻辑太多太细, 导致要加入的流水线寄存器操作也太多, 可能性能反而下降
  3. 稍后要说的流水线数据/控制冒险(带反馈的流水线: 数据相关或者控制相关造成的冲突)

Y86-64 流水线实现: PIPE

加入流水线寄存器

首先需要把更新 PC 逻辑放到每个时钟周期的最前面称为 SEQ+ 处理器硬件结构
因为后续需要有分支预测之类的, 这里的 PC 更新逻辑还要通过很多其他阶段传回来的值 Predict 等

然后回看流水线的原理, 就是拆分组合逻辑, 加入流水线寄存器
最大化地避免出现控制混乱的情况肯定是要按 Fetch/Decode/Exec 等处理器阶段来拆分
(比如说把 Fetch 的读 rA 和读 rB 拆到两个组合逻辑里, 这他妈不傻逼吗?)

那么要做的就很明确了: 在每个处理器阶段的中间插入流水线寄存器保存当前的状态,
当然要注意每一个阶段的流水线处理器都是保存了需要保存的全部状态的, 比如每个阶段的流水线寄存器都有 staticode 寄存器
还有就是这是流水线寄存器不是 rA, rB 这种程序可见状态的寄存器

给这些流水线寄存器以其所在阶段的前缀命名:
执行(Execute)阶段的 valA 流水线寄存器就是 E_valA

整体的流水线执行 be like:

1
2
3
4
5
6
7
8
9
10
11
12
13
1 2 3 4 5 6 7 8 9
F D E M W
F D E M W
F D E M W
F D E M W
F D E M W

周期五:
W(I1)
M(I2)
E(I3)
D(I4)
F(I5)

预测下一个 PC

上面我们也说了不同指令更新 PC 所用的不同值, 不过这里还有一个分支预测的问题
Y86-64 所采取的分支预测策略是”始终预测选择了条件分支”, 不过 ret 是从内存里取地址, 所以不能预测
加入 Predict 的块用来预测 PC

流水线冒险的处理

$(分类讨论一下每个存储器冒险的可能性)

如果指令A B连续执行并且B依赖A的数据, 则出现流水线数据冒险
此时有两种处理方式: 暂停(插入气泡) 或 转发

  1. 暂停是在某个阶段检查到出现数据依赖, 就插入一条控制令(气泡)代替这条指令执行
    实际上也就是”等一会, 等依赖的数据写回寄存器了在继续执行”而”在这个等待的过程中执行这条空白指令使流水线继续满载”
  2. 转发是发生数据依赖时不等到写回寄存器再继续执行获取值了, 而是直接接收执行阶段计算完的结果(通过我们的硬件设计转发过来)

有一种数据冒险不能单纯用转发解决, 即加载/使用冒险
本质原因是发现数据冲突到结果被计算出来的间隔时钟周期太短,值还没计算出来当然不可能转发了,
所以这时候同时使用暂停插入气泡+转发,
这种方法叫做”加载互锁”

控制冒险

控制冒险仅仅发生在 Fetch 阶段无法获得下一条指令的地址时, 有几种情况
1. ret 和 jmp, 需要至少到访存阶段之后才能获取 2. 分支预测错误 :(

  1. 对于 ret: 流水线暂停到 ret 写回阶段完成即可继续 Fetch 下一条指令

  2. 对于分支预测错误: Y86-64 默认预测选择分支, 不过最多两个时钟周期后就能知道分支预测是否正确(jne 之类的指令的执行阶段),

也就是说错误的指令顶多执行到了 Decode, 而这一阶段执行完(到下一阶段时钟信号上升)才会造成影响(设置了条件码),
满载流水线每个周期都会有一个 W 阶段完成并有一个 F 阶段进入, 所以两个周期有两条错误指令, 一条在 D 阶段, 一条在 F 阶段
所以只要在第一条错误指令的 D 阶段和第二条错误指令的 F 阶段插入气泡即可

异常处理

主要有两个原则: 1. 通过在流水线中传递 stat 状态码使异常状态和该指令的其他信息一起在流水线中传递。 2. 导致异常后禁止任何指令更新条件码寄存器

  • 多条指令引起异常优先报最深的指令(因为最早)
  • 取消了一条可能引起异常的指令时, 由于是在流水线中传递异常信号, 可以在下一条指令 F 阶段时把错误的指令的后续阶段填成气泡取消掉

PIPE 各阶段的实现

  1. 首先最大的区别是寄存器前面都要加前缀, 区分不同阶段的流水线寄存器(SEQ 只有一个全局的寄存器状态, PIPE 有五种)
  2. 取指阶段要考虑是否跳转/是否是ret, 如果都不是就是 f_predPC, 即预测的下一个 PC (由于默认预测选择分支, 所以如果是跳转指令就跳 f_valPC, 否则就是正常的 f_valP)
  3. 由于没有指令同时需要 valPvalA, 所以通过一个 Sel A 单元(是否选择 valA)直接合并为一个 valA 信号了, 不过流水线还要加一个变成 Sel + Fwd A 单元, Fwd 用于之前说的转发
  4. 执行阶段要考虑一下异常处理, 如果有异常就继续通过流水线传递, 但是要禁止程序修改条件码, 并且插入气泡暂停流水线

流水线控制机制

  • 流水线寄存器的暂停和气泡信号: 暂停信号设置为 1 时锁定存储内容输出本来存的数据, 气泡信号设置为 1 时随时钟上升输出 nop
    以此来解决 处理 ret/加载/使用冒险/分支预测错误

  • $(控制条件的组合)

  • $(流水线控制逻辑的实现) # 单独操作流水线寄存器, 通过覆盖流水线正常指令处理上述特殊条件

性能分析

(2)CPI=Ci+CbCi=1.0+CbCi=1.0+lp+mp+rp

后面三个分别是三种特殊情况的惩罚, 计算方式为该原因引起插入气泡的总数除以执行次数的总数

PIPE 流水线处理器未实现的

  1. 多周期指令, 发射到特殊单元(例如同样流水线化的浮点单元)然后同步回来
  2. 与存储系统的接口, 比如说第六章的高速缓存和第九章的 TLB