寄存器堆 · 两个读口与一个写口

32 个寄存器 × 32 位 → 5 位寄存器号,读不花时钟,写等一个沿
✎ 写你自己的程序 —
写法:add R5, R1, R2 / sub / and / or / xor,三操作数写成「目标, 源1, 源2」; 装常量用 li R10, 0x1234ABCD(十进制、0x、0b、负数都认,-1 就是 0xFFFFFFFF)。
寄存器写 R5、r5 或 $0 都行;# 开头是注释,空行忽略。右边一列是逐行结果预览,写错的行会直接标出来。
每条指令 = 一个时钟沿;← 后退会把这一个沿改过的寄存器、拍数、轨迹全部回滚,可以反复来回试。
核心:一条算术指令 R{rd} ← R{rs} OP R{rt} 需要同时拿到两个源操作数,所以寄存器堆天生有两个读口; 结果要存回去,所以有一个写口。32 个寄存器只要 5 位编号,没有 tag、没有 offset、没有块。
最反直觉的一点:读口是组合逻辑,不消耗时钟。你把 rs 从 R1 改成 R5,A 口上的数据当拍就变,一个沿都没等; 而写口必须等到上升沿才真的把值锁进寄存器。同一个部件,两个口一个「立即」一个「等一下」。
怎么玩:上面可以自己写程序,逐行执行、逐行后退;点 5 位编号上的任意一位直接翻转它;点每行右边的 A / B / W 小方块, 把该寄存器接到读口 1 / 读口 2 / 写口,再按 ↑ 打一个时钟沿手动试。
123 选 rd / rs / rt,↑↓ 加减编号,回车 打沿,←→ 后退 / 前进,W 切 WE,X 复位。
值得亲手试的三件事:①写 add R1, R1, R1,rd 和 rs 是同一个寄存器,本拍读到的仍是旧值; ②把目标写成 $0,写入被静默丢弃,R0 永远是 0;③写 li R5, -1 再 add R6, R5, R1,看看无符号回绕成什么样。
展开:端口结构、同拍读写语义、$0 为什么免费、以及读口到底多贵
端口方向触发方式延迟为什么需要
读口 1(A)出电平(组合)译码 + 多路选择,~1 个门延迟ALU 的源操作数 1
读口 2(B)出电平(组合)同上ALU 的源操作数 2
写口(W)入上升沿 + WE需要一个完整时钟沿把结果存回去

同拍读写的语义:读口是「一直盯着某个寄存器的窗户」,写口是「只在沿上开一次的门」。 所以一条 R3 ← R1 + R3 在同一拍内:先(组合地)读出 R1 和 R3 的旧值送进 ALU,算出和, 到这一拍末尾的上升沿才把和写回 R3。本拍读不到本拍写的值 —— 这不是缺陷,而是时序电路的定义。 到了流水线,这条性质就变成了 RAW 数据冒险:写回发生在第 5 级,而下一条指令在第 2 级就要读它,中间差 3 拍,只能靠转发(forwarding)或停顿补上。
$0 为什么免费:R0 不是「存了一个 0 的寄存器」,而是读口上直接接地、写口干脆不接。 这样任何 R0 作为源就白得一个常数 0,不需要额外的「清零」指令,也不需要占用一个真实寄存器; 把结果写进 R0 就等于「算完丢掉」(用来做比较、占位),而且不会报错。代价只是译码器要特殊处理编号 0。
读口有多贵:32 选 1 的 32 位多路器,每位要 31 个 2:1 选择器,32 位就是 992 个;两个读口接近 2000 个 2:1 选择器, 而存储本体 32×32 = 1024 个触发器。读懂的成本和存下的成本同量级 —— 这就是寄存器堆只能有 32 个、 而且编译器要做寄存器分配(尽量把变量留在寄存器里、减少来回搬运)的原因。想加第三个读口,面积几乎再翻一半。
为什么不用内存代替:内存一次只能给一个数据(单端口),取两个操作数要两拍;而且要走地址、访存、等 CAS。 寄存器堆的组合读是「同一个门延迟」,这是它能塞进 CPU 关键路径的前提。