2025-11-08
4.1 Y86-64 指令集体系结构
程序员可见状态有:寄存器(RF),条件码(CC),程序状态(Stat),DMEM(内存),以及 PC.
没有 r15,而 F 则代表了“空寄存器”. 这是为了简化后面的 seq 实现.
指令有:
OPQ,HALT,NOP.
RMMOVQ, MRMOVQ. IRMOVQ,RRMOVQ. CMOVQ.
PUSHQ / POPQ.
RET / CALL / jXX. 这些和跳转有关.
和栈有关的指令有 PUSHQ / POPQ / RET / CALL.
和 cnd 有关的指令有 jXX / CMOVQ.
异常有:
- AOK:对!!!
- HLT:遇到 Halt
- ADR:非法地址
- INS:非法指令
RISC vs CISC
- RISC 指令数量少,寻址简单(只有基址&偏移),没有较长延迟的指令,并且指令编码都是 4 字节.
- RISC 遵循 load/store 体系结构,只能对寄存器操作/逻辑运算,从内存读取只能 load/store. 另一方面,RISC 寄存器很多.
- RISC 实现细节是可见的. RISC 没有条件码.
4.2 HCL
AND 门:|二)
OR 门:)二>
NOT:▷·
时钟寄存器:只有在高电位的时候会读取输入信号,并且直到下一个时钟上升沿,这个值便一直会是寄存器的输出.
寄存器文件有两个读端口和一个写端口.
4.3 SEQ
seq 的阶段:取指(fetch),译码(decode),执行(execute),访存(memory),写回(write back),PC Update
取指阶段:固定的 icode:ifun <- M1[PC],可能有的
rA:rB <- M1[PC+1],以及可能有的
valC <- M8[PC+2]. 然后再据此更新
valP <- PC+?
SEQ 实现中需要注意 icode 有 -> PC 增加器的接线,以及 icode 会产生 invalid instruction 的信号,传递到 stat. 会产生 imem error.
译码阶段:做 valA <- R[rA] 和
valB <- R[rB] 这样的操作.
RF 中 rA 会贡献给 dstM 和 srcA,rB 会贡献给 dstE 和 srcB. 注意 cnd 也会返回来传输到 dstE.
涉及到栈操作时,vB 通过运算得到新 rsp. ret/pop 的时候 vA 和 vB 都要读自 rsp. call/push 的时候只需要 vB 读自 rsp 来更新 rsp,当然 push 的时候需要 vA 来自 rA.
执行:注意是 valB OP valA. 以及如果是 ALU 指令需要
Set CC. 对于 jXX,需要
Cnd <- Cond(CC,ifun)
ALU A 的来源只能是 valC 和 valA(也可以是 \(\pm 8\)),ALUB 的来源只能是 valB. 另一方面,ALU fun 的来源是 icode & ifun,icode 参与 setcc/ALUA/ALUB 的逻辑. cnd 信号的逻辑是,先 setcc,然后 setcc & ALU 进入 CC 逻辑,然后 CC & ifun 进入 cond 逻辑,然后出来得到 cnd.
访存:做 valM <- M?[??] 或者
M?[??] <- ??
mr / rm / push / pop 时,通过 rB 和 valC 参与执行得到的 dstE 作为 address;而 call / ret 的时候则需要 vA 作为 address.
rm / push 时,通过 rA 作为 data;call 时则需要 valP 作为 data.
mem write / mem read 逻辑以 icode 作为输入,来得知是要 write 还是 read.
会产生 dmem error.
写回:做 R[??] <- ??
更新 PC:PC <- valP / valM / valC. jXX 需要根据 Cnd
做判断.
计算 newPC 需要 icode, cnd, valP, valM 和 valC. 这个逻辑是简单的.
4.4 PIPE
吞吐量:(1 / 一条指令的延迟(ps))* (1000ps / 1ns) = 吞吐量 (GIPS)
流水线的延迟 = max 组合逻辑部分延迟 + 寄存器延迟.
异常处理
问题 1:可能有多个异常.
办法:最深的指令引起的异常(即最早出现的异常),优先级最高.
问题 2:分支预测错了出现异常,更正后异常被取消了.
办法:stat 跟着流水线走,显然如果取消了那异常就不会走到 W_stat.
问题 3:异常出现了,但是后面的指令(eg.E)在异常被发现(eg.M)之前已经更改了系统状态.
办法:只会发生在 m_stat / W_stat 出现异常的同时 E 在写 cc. 于是把这两个元素加入写 cc 的逻辑即可.
流水线 HCL
PIPE 每组寄存器记录着目前正在执行的阶段的指令的信息,并且介于这个阶段和上一个阶段之间.
F 寄存器较为特殊,记录着由上一次 F 阶段取得的信息得到的关于 PC 的预测值.
其余的寄存器每个寄存器都有 stat 和 icode. ifun 在 M/W 阶段显然是不被需要的.
F 阶段:
从 F 寄存器得到 PC 的预测值之后,需要经过一个 Select PC 的逻辑算出应当出现在这轮的 PC.
这个 select PC 逻辑综合
M_valA和M_Cnd(jXX),W_valM(ret)然后会根据已有信息去更新下一轮的 PC 预测值. 只有在 jXX 和 call 的时候会设成 valC,其他都是直接做 pr increment.
这个阶段可以发现非法指令/HALT/指令地址越界. 但是非法数据地址需要推迟到访存才能发现.
D / W 阶段:
主要点在于 foward. foward 的源有五个:
e_valE,m_valM,M_valE,W_valM,W_valE. 判断 foward 的决策先后顺序应该按照这个. E/M/W 应该先判更前面的(因为这代表着更近期的指令),这是容易的. 而至于一个阶段内的 valM 和 valE,应该先判 valM,否则会在popq %rsp的时候出错(我草这个唯一反例也太阴了,有任何人会这么写代码吗).W 阶段还有一个点,在于由于要用 W_stat 表示整个程序的 stat,所以需要把
SBUB(气泡)重新改为SAOK.
E 阶段:
这个没啥区别. 一个注意的点是 cmov 和 jxx 是在进行到 E 阶段,看到 CC,然后通过 Cond 逻辑得到 cnd. 理论上这个 cnd 可以直接返给之前的 select PC,但是这会增加依赖链长度. 于是我们选择直接传递到 M.cnd,再后一个阶段再传回去.
另一个注意点是 m_stat 和 W_stat 需要传进来当作 set cc 的参数,因为我们不希望在出错的情况下更改条件码.
M 阶段:
原本的 valP 不被需要了,这是因为在 E 阶段就已经把 valP 给融合到 E.valA 里面去了,所以所有 data 的来源都是 M.valA
我们还需要操作一下 dmem_error. M 阶段很多处于 M 寄存器的值都要被提供给电路的其他地方.
流水线控制逻辑
stall:忽略该寄存器的输入,维持其原本的输出. 作用是对于一个阶段,下个时间仍然洗当前时间已经洗过的车.
bubble:让该寄存器的输出变成 nop. 作用是让下个时间该阶段直接变成 nop.
RET
会在 D 阶段发现,但是需要等到 M 算出来之后(为了减少路径长度)在
W.valM才能进入f_pc逻辑. 故在D/E/M是 RET 的时候,此时执行 fetch 的一定是错的,故需要d_bubble. 教材上写要f_stall, 实际上不 stall 也没问题.
JX
我们会在
e_cnd发现预测错误. 同样,在M.cnd进入f_pc逻辑. 故E为 JX 且!e_cnd的时候,此时 D 和 F 也是错的,故需要d_bubble和e_bubble.f_stall和 ret 一样.
注意到其实可以更晚一回合,在 M.cnd 处再发现 JX.
另外需要额外注意,后面读进来的 RET 的所有控制都应该直接被忽略.
load / store
只有
E=popq/mrmovq且E.dstM=d_srcA/d_srcB的时候会出现问题. 这种情况下,只能让D和F都停一下,然后在E放入 bubble,等一回合.
异常
当异常进入 M(
m_stat / W.stat)的时候,就意味着事情开始变得不对了. 我们给 M 插 bubble,防止更新;另一方面,W.stat的时候需要在 W stall,停止执行.