这周肝作业肝得差不多了,也快到考试周了,终于抽时间把第四章弄了弄,写了一份总结。发上去就吃晚饭!
不一定有时间定期更新这样的东西,毕竟是个卷心菜,总要担心自己忙不忙得完23333
文章目录
Y86-64指令集
instruction | access memory | need valC | need regids |
---|---|---|---|
halt | |||
nop | |||
rrmovq | yes | ||
irmovq | yes | yes | |
mrmovq | yes (read) | yes(offset) | yes |
rmmovq | yes (write) | yes(offset) | yes |
Opq | yes | ||
jXX | yes | ||
cmovXX | yes | ||
call | yes (return) (write) | yes | |
ret | yes (return) (read) | ||
pushq | yes (write) | yes | |
popq | yes (read) | yes |
两个特殊的(暂停和跳过),四个移动,一个计算(多个功能),两个条件(条件传送和条件转移),四个涉及栈指针。
Y86-64的指令分阶段实现
实际上没有必要记住每一个指令的细节,只需要知道大体的指导原则。这样才能面对错综复杂的新指令。
1 Fetch
取指阶段遵从一致的原则:
- 总是读取
icode: ifun
(split
) - 根据需要读取寄存器
rA: rB
和valC
(align
) - 根据指令长度调整
valP
(need_valC
,need_regids
)
取指阶段的硬件逻辑如下:
- 访问内存可能产生
imem_error
- 合法的指令
icode
才能产生instr_valid
2 Decode & Writeback
译码和写回阶段都要对寄存器文件操作,其硬件逻辑如下:
- Decode时需要从寄存器文件读出
valA
和valB
,Writeback时可能写Execute阶段的valE
,可能写回Memory阶段的valM
。 - 根据
icode
来决定选择rA
,rB
还是其他寄存器名,valA
不一定来自R[rA]
,比如popq rA
。 - 所有不经过内存的写回都使用
valE
,即使不需要计算也要加个0! - 通常来说,
rA
决定了内存的写回端口,rB
决定了其他的写回端口。只有popq %rsp
用到了两个写回端口,涉及到先后顺序的问题,这个先后是寄存器文件的硬件决定的。 cmovXX
如何实现条件传送?就是根据Cnd
调整dstE
为RNONE
。- 涉及
%rsp
的几条指令不太好理解,总结一下:- 访存阶段,
mem_addr
只能是valE
或valA
,mem_data
只能是valA
或valP
。其中通用的规则是:valE
存储Execute阶段修改过的偏置地址或栈指针,valA
保存原来的栈指针或待写入的值。注意到valA
和valP
不会被同时使用,只有call
会使用valP
,所以在PIPE阶段这两个信号被合并。 mrmovq
和rmmovq
比较简单,不涉及栈指针的变化,以下就不讨论了。- 如果是读取内存,重点关注两个操作
popq
和ret
。这两个操作都会使用操作开始前的%rsp
访问栈,本来就如此,栈顶元素在那里摆着呢。那么valE
干什么呢?它负责写回阶段调整栈指针的值。需要注意的是,x86-64保证popq %rsp
执行完后%rsp
存储指令执行前栈顶元素的值,y86-64继承了这一点。 - 如果是写内存,重点关注
pushq
和call
。他们俩不需要使用原来的栈指针,所以pushq
的valA
用来保存待写入的值了,call
直接没用valA
(因为它要用valP
)。如果是pushq %rsp
,它会把原来栈指针的值压栈,再把栈指针加8,这涉及到所谓“从不回读”原则,一个操作的完成不依赖于这个操作的结果(看起来像废话)。
- 访存阶段,
3 Execute
执行阶段的逻辑比较简单。
aluA
可以来自valA
或valC
,aluB
只能来自valB
。当然,它们可以是常数。CC
是ALU单元产生的条件码信号,cond是决定是否更新条件码的硬件单元(根据ifun
和CC
),Cnd
是条件码寄存器的值。alufun
只在OPq
中被设置,其他情况默认是加。
4 Memory
- 内存访问的地址
mem_addr
和数据信号mem_data
在上面讲过了,不多讲。 mem_read
和mem_write
信号决定是读还是写,注意到没有指令又读内存又写内存。- 到这一阶段产生
dmem_error
,所有可能报错的点都报完了,结合取指阶段的报错和指令,更新整个处理器的状态码Stat
。到了PIPE还是这个思路,流水线寄存器始终保存当前整个处理器的状态码(当然,为了逻辑上的一致,它到最后的W阶段才修改状态码寄存器),并以此指导整个流水线控制逻辑。
5 PC Update
- 需要五个信号
icode
,Cnd
,valC
,valM
,valP
。回头把代码补一下。
Extra Exercises
我把每个阶段核心的要点列出来了。回头再把这些单独做一遍。
srcA
和srcB
的代码?aluA
和aluB
的代码?mem_addr
和mem_data
的代码?mem_read
和mem_write
的代码?stat
的代码?new_pc
的代码?
Pipeline的通用原理
- 流水线的延迟单位通常为 p s ps ps,而吞吐量单位通常是 G I P S = 1 i n s t r u c t i o n ⋅ n s − 1 GIPS = 1 instruction \cdot ns^{-1} GIPS=1instruction⋅ns−1。所以算吞吐量要补个系数 1000 p s 1 n s \dfrac{1000ps}{1ns} 1ns1000ps
- 流水线处理器的延迟取决于最长的阶段开销和寄存器的延迟。不能说某一个阶段算完了这个周期就提前结束了!
- 流水线不能太深,否则寄存器延迟占比太大。
SEQ to PIPE
PIPE-
从SEQ到SEQ+再到PIPE-,核心的工作就几件事:
- 把取指和更新PC值,预测PC值合并到一起作为一个阶段,指令执行过程整合成五个阶段
- 插入流水线寄存器
valA
和valP
用selectA
合并,减少电路复杂度。
剩下的工作全是在处理各种hazard。
Data Hazard
避免数据冒险最简单的方法就是暂停流水线。比如说,我发现data dependency,后面的指令需要等一等再执行,那么我让F和D阶段的寄存器stall,他们这周期都不更新,下一周期又把这周期干的事又干了一遍。E阶段插入一个气泡,相当于代码当中多了个nop
。那么,流水线后面的部分正常执行,会不会对取指有影响呢(这是最有必要的一个转发)?答案是并不会。大多数情况下predict_pc
的值是准确的,根本不依赖上面两个转发。除非我们面对的是ret
或者预测错误的jXX
,这时候这个问题就变成多个hazard同时出现了,这个留到后面有更具体的方案,我们会解决它。
stalling对性能损害太大,所以有forwarding 的解决方法。这个不详细解释了,注意顺序,待会自己推一遍Sel+FwdA
和FwdB
是怎样实现的。唯一需要注意的是E阶段的用的是小写的,因为e_dstE
和E_dstE
在cmovXX
指令中可能不一致。
在我们的设计里,stalling可以解决数据冒险的所有问题,但forwarding不行,因为不允许内存中读出的值直接转发到E阶段。所以会有加载互锁load interlock。
Control Hazard
控制冒险有两个,一个是ret
,这个在我们的实现中没有给预测,就三个bubble硬等;另一个是jXX
预测错误,在E阶段检查到时要立刻在D和E插入两个泡泡,然后到下一周期jXX
进入到M阶段时,新一个指令的取指一定会从jXX
的转发取指(所以没有必要再F阶段插入bubble)。
Pipeline Control Logic
流水线控制逻辑,它一共会面临几种情况:
- load/use hazard
- ret
- misprediction branch
- exception
前面三个简单来说就是插泡泡和暂停。如果需要插入nop
,那么应该插入一个bubble并让前面的阶段都暂停;如果是要实现instruction squashing,那就把需要清除的指令用bubble覆盖,后面的取值仍然可以正常进行(转发会保证predict_pc
用不上)。
比较不常见的是exception,单独说一下。在SEQ中我们说了,直到M阶段,我们才能拿到当前整个处理器的状态码;在PIPE中,直到W阶段我们才写回Stat。我们的控制逻辑遵从了最基本的原则:后面不应该做的指令不应该修改全局状态,并采取了一种比较省事的策略:只要全局状态不被修改就完了。全局状态有三个:寄存器文件(到W就停了,不用管)、内存(M发现问题)、条件码CC(E阶段算,E阶段存)。所以我们必须保证当前周期不写回内存,也不修改条件码CC。
多种情况的组合,就看你想干啥了。ret
和错误分支的比较简单,它俩加在一起就是我想要的结果。但是ret
和load interlock需要关注一下,他们在D阶段一个插bubble,一个插暂停。例如,前一条指令是popq %rsp
,后一条指令是ret
,我希望的行为是让流水线停下来,确定到底该返回啥,而不是让ret
开始咣咣咣插bubble,否则ret
拿到的值是不对的,M阶段又没办法向E阶段转发。这样的分析思路,对于新加入的指令也是适用的。(做过往年题后写这篇总结,我很关注分析问题的思路。)
Extra Exercise
predict_pc
和select_PC
?d_valA
和d_valB
?- 四个控制逻辑的触发条件?
- 什么时候需要向哪个阶段插入bubble/stall?控制的逻辑是什么?