北航CO 2024 P5课上部分题目回忆与分析
你好!本文章为该博客的第7篇文章!
本文章是对课上部分的题目回忆(这次真的只有部分,因为关于T3我能说很大一段),不保证回忆内容一定正确,请各位注意甄别!
以及,如果本文章涉及学术诚信问题,请第一时间和我联系,我将及时做出修改,谢谢支持!
P5课上题目(T3)回忆
三题仍然为互相独立的添加指令题,但是这次我只写T3的回忆。
准确来说,我只想写这次上机里有含金量的题,T1和T2T3相比有含金量的部分几乎为0,T2又只有一个清空延迟槽可以说道说道,于是我只想完整回忆T3,至于清空延迟槽,我会在写T3正解前先写一下大概内容。
T3添加指令为 lwoc
,I型指令。具体操作为,从DM中读取起始地址为 的一整个字,若该结果小于无符号32位整数 0x80000000
,则取该结果的低4位进行零扩展后作为写入 GRF
的地址,否则取指令中的 rt
为写入 GRF
的地址,写入数据均为该结果。
对CPU模块编写的几点建议
- 尽可能让主模块仅起到连接各模块的作用(除转发和阻塞信号对各级流水线寄存器的控制外),这样在对电路进行修改时,我们可以把精力只放在对各个模块内部的修改,而无需过度置疑CPU连接的正确性。
- 课下的时候控制信号请务必集成到控制器里(包括Tuse和Tnew),能不新开模块就别新开模块生成控制信号。
- 转发和阻塞的判断请务必使用各类控制信号和rs/rt/写入地址等等进行判断,而不使用完整的指令在内部做判断。(除非你是临时加指令)
- 为了防止Verilog的特性导致产生笔误,从而在无意中创造出一个和你预期的信号不同的wire型变量,建议在每个模块名前加上``default_nettype none`。
- 实现时务必注意IM和DM的数据大小。
清空延迟槽
这一部分主要是对课程组未提及(或者提及了但是没细说导致我没看到)的内容进行补充。
首先,我们说一下清空延迟槽是个啥意思。首先,你已经知道了在处理分支跳转类型的指令时,我们会把这条指令的后一条指令放入延迟槽中,并且在课下内容里这条指令一定会被执行。那么,假如我们新加入的指令里有不希望这条指令执行的情况呢?我们会选择把刚刚进入F级的这条指令从CPU扔出去。这就是清空延迟槽了。
那么,我们应该如何把这条指令扔出去呢?
实际上我们只需要做两件事:
- 不让这条指令流到D级,而是让一个气泡流入D级,也就是发一个清空信号到F级到D级的流水寄存器。
- 让D级进入一条新的指令,即维持PC继续变化。(其实这个不会被显式的做出来)
结束了吗?还没完呢。
实际上,有些指令是需要根据跳转条件的结果来决定是否清空延迟槽的,这就产生了一个问题:如果这条分支跳转指令被阻塞了,那么这个周期实际上没法判断延迟槽是否清空,所以此时我们肯定会保留延迟槽的指令,等到我们可以判断跳转条件是否成立的时候再做决断。
所以,对于这种情况,我们需要额外判断一件事:仅当当前周期阻塞信号为0时,我们才尝试清空延迟槽。
好了,这就是T2最有含金量的内容了。
LWOC 题目分析
想一步到位直接AC这题很困难(能预知未来的就别发话了),我会把自己A这题的过程分为三部分细说。
为了防止大家看不懂所以把我课下设计的模块放上来
- 程序计数器(PC):记录程序当前运行的指令所在存储器位置。
- 指令存储器(IM):根据PC的值,从ROM中读出指令。
- 控制器(Controller):确定CPU各模块写入使能和写入/运算数据源。
- 通用寄存器组(GRF):读取最多两个寄存器的值,向最多一个寄存器写入值。
- 分支控制器(BranchController):计算并选择分支跳转的目标地址。
- ALU立即数扩展(ALUExt):确定ALU立即数扩展方式,对立即数进行扩展。
- ALU运算数选择器(ALUMux):根据ALU操作数源,选择ALU的操作数。
- 算数逻辑单元(ALU):对ALU操作数进行运算。
- PC写入值选择器(PCMux):选择下一周期PC的值。
- 数据内存(DM):进行内存读写。
- GRF写入地址选择器(GRFAddrMux):选择下一周期写入的寄存器。
- GRF写入值选择器(GRFMux):选择下一周期写入寄存器的值。
- 还有各级流水线寄存器,不过我似乎在这篇文章里没有用到。
如何做对这题(但是CPU跑不快)
首先,我们会发现一个很悲伤的事情:直到我们读出DM的数据之前,我们甚至都不知道自己要写哪个寄存器,那么当这条指令进入E级与M级中时,我们压根都不知道是否应该对之前的指令进行转发或者阻塞,仅在W级的时候才能知道这一点。
所以一个很自然的想法就是,只要在E级或者M级出现了这条指令,那么我们就认为需要对D级进行阻塞。而具体W级的时候处理出具体写了哪个寄存器,那就在GRFAddrMux里加点逻辑就行了。
如果你的课下实现正确,并且课上按照这个思路实现也没错,那么恭喜你!
你做对了,但是你做错了。
你AC了九个点,这题一共十个点,那个没过的点会说:你TLE了。
如何让CPU跑快(但是这题做不对)
于是你会思考如何修改一下暴力阻塞,使之更聪明。
原题目里其实强调了一件事情:
由于是取该结果的低4位进行零扩展,所以此种条件下可能会被写入的寄存器只有
GPR[0]
到GPR[15]
这16个寄存器。
于是你大喜过望,这不就是在暗示你可能被写入的寄存器只有这16个外加一个 rt
嘛!
也就是说,如果这条新指令出现在了E级或者M级,但是D级的指令并不涉及对以上涉及的寄存器的读入,那么我们依然是可以让这条新指令流入E级的。
于是我们会修改控制阻塞信号的单元,把上一部分提到的暴力阻塞改成这种“这条指令出现在E级/M级且(D级某个要读的寄存器是 GPR[0]-GPR[15]
中的某一个或要读的寄存器是GPR[rt]
)”的时候就阻塞。
然后你把这个写法提交上去,再次恭喜你!
那个TLE的点你过了,但是同时你实打实的WA了一堆点。
而且你会发现,这些WA的点不约而同的指向了同一个问题:你尝试往 $31
写入一个值,而评测机希望的是往不知道哪个寄存器里写入一个什么值,总之不是 $31
就对了。
最终版本——问题还是阻塞条件!
然后你就会联想到,会不会是一个 jal
跟一个 lwoc
就会触发这个问题呢?于是你写了一组样例,发现:
还真是。
并且具体表现是,你的阻塞信号莫名奇妙的变成了一个 x
!
好,把上面所有的主语“你”换成“我”就是我跟这道题缠斗了1h的结果。
在我完成的CPU中,由于这条新指令在E级/M级时我并不能确定写入了哪个寄存器,所以我在把这个信息传入阻塞单元的时候,传入的实际结果是一串 x
。(下面为了叙述方便,我只举E级的例子,M级同理)
然而,在原先对E级的阻塞中,我并未考虑传入结果是 x
或者 z
的情况。这也就是说,如果我在此时判断D级要读的寄存器是否为E级要写入的寄存器,那么判断结果就会是单个 x
,于是这个 x
就会一连串的传导到阻塞信号,并最终酿成大祸。
解决方法……很简单。而且有两种。
- 我们采用
===
替代==
。===
是Verilog中针对四态值的判断,会对x
和z
也正确的进行判等比较,而==
在遇到比较内容为x
和z
的时候,会无脑产生一个x
出来,最终导致上述结果的产生。 - (考场思路,不是很好,大家自行考量)我们选择在遇到这条特殊指令的时候,直接不采用课下写的阻塞的结果,转而新开一个判断条件得出阻塞结果。
然后,你AC了。
反思课下设计模块的问题
三等号,目前能想到的只有这个了。
反思课上表现的问题
实际上,T1和T2我的表现都没啥大问题,加起来也就花了半小时。
但是对于T3,我最大的问题是,没有对课上指令的优化做任何的心理预期(尽管我的设计里已经预留了一点改动空间,但是从上述错误来看,实际上这还是不够充分),从而在前几次WA的时候慌了神,甚至都不知道看报错信息和构造测试数据了。
这种不好的心理状态直到第2发还是第3发WA的时候才结束,那会我刚刚把自己的心态稳住,让自己意识到“没必要那么急”。虽然那个时候已经是八点了。
然而看报错信息的事情还是八点十几分才做的,这个需要改正。
到熄灯时间了,我先睡了,有啥想到的明晚更新吧。没有的话,这句话也就撂在这里不管了。