从指令状态到缓存写回:同一程序的两份执行账本
这份任务让一段短程序贯穿两个实验。先确认哪些指令真正完成、每次load得到什么;再把它产生的同一条数据访存轨迹送入两个缓存,核对命中、被逐出的块和脏数据。错误路径上取到的store不得混进轨迹。
主线有九站,默认你已经会读位串与简单汇编。如果数值表示还不稳,可先读定宽整数、扩展与截断和字节序;想检查机器码时读指令字段;想知道级间寄存器为什么这样更新,读时钟边界与单周期数据通路。这些是按需补课入口,不必在已经熟悉的内容上绕行。
题目与固定模型
ISA、环境和程序
使用RISC-V文档v20260120中RV32I 2.1的add/addi/lw/sw/beq正常执行子集。寄存器和PC为32位,算术和地址取低32位,x0恒为零。数据按字节寻址,小端,全部访问是有效且四字节对齐的普通内存。本例没有地址环绕、异常、设备或自修改代码。代码放在从0x1000开始的独立只读区域;表中PC列只写相对0x1000的偏移,实际架构PC等于基址加该偏移。数据地址均为绝对地址,初值约定只覆盖数据区。指令与数据使用独立访问端口,并不意味着ISA拥有两个同号地址空间。
初始架构PC=0x1000(相对偏移0)。数据区全部为零。初始x2=5、x4=1、x6=9、x7=11、x8=13,其他寄存器为零,包括作为基址的x1。
| 标签 | PC偏移 | 指令 |
|---|---|---|
| I1 | 0x00 | lw x2,0(x1) |
| I2 | 0x04 | add x3,x2,x6 |
| I3 | 0x08 | sw x3,4(x1) |
| I4 | 0x0c | lw x4,32(x1) |
| I5 | 0x10 | beq x4,x0,+12 |
| J1 | 0x14 | sw x7,0(x1) |
| J2 | 0x18 | addi x6,x0,99 |
| I6 | 0x1c | lw x5,0(x1) |
| I7 | 0x20 | sw x7,64(x1) |
| I8 | 0x24 | lw x9,32(x1) |
| I9 | 0x28 | lw x10,4(x1) |
| I10 | 0x2c | sw x8,8(x1) |
| I11 | 0x30 | lw x11,40(x1) |
| I12 | 0x34 | lw x12,8(x1) |
| I13 | 0x38 | lw x13,64(x1) |
到PC偏移0x3c(实际0x103c)时停止取本片段,并让已有流水指令排空;这不是增加一条RV32I halt指令。
五级时序
- 单发顺序IF/ID/EX/MEM/WB;各级内容指本周期开始时的指令,级间结果在周期末保存
- 指令存储、数据存储各有独立端口,固定一周期;不把下面缓存实验的miss代价算进这张流水图
- WB前半写,ID后半读;EX/MEM非load结果和MEM/WB最终结果可送往EX,最近较老写者优先,x0不接受转发
- load在MEM末生成数据,下一周期才可用于EX;store的base与data都在EX确定,data随EX/MEM携带,不设MEM阶段晚旁路
- load-use时保持PC和IF/ID,下一ID/EX插气泡;旧EX/MEM和MEM/WB照常推进
- beq在EX末用转发后的操作数决断,按未跳转取指;taken清除本周期ID与IF两条年轻指令,下一周期IF取目标
- valid=false的气泡没有寄存器或内存副作用;store在MEM末写内存,store/beq的WB只作为空操作占位
两个缓存
两个配置都用32位字节地址、32 B数据容量、8 B块;容量不计元数据。D为四组一路,A为两组两路。都冷启动、WB+WA、缺失先保存脏victim再填完整块、LRU命中与填入均更新。先用无效way,多个无效way选最低编号。每次访问为一个不跨块的四字节word,按程序数据trace串行完成。
本题刻意选择自然对齐的正常访存;想改变该条件,可接对齐与跨界访问。若要让某条load故障,则接精确异常,另核对异常前缀与错误路径故障取消,不能直接沿用本题无异常时序。
任务要求:
- 顺序执行程序,列出最终寄存器、内存和11个数据访问;解释J1/J2是否完成
- 从空流水开始列每周期状态、保持/气泡/清除及转发来源,核对最终状态与顺序执行相同
- 对同一trace计算两配置的tag/index/offset、命中、victim、dirty写回以及最后驻留内容
- 分开报告需求传输与终末flush,并给出取消互锁、取消flush或误用新tag回写的具体错误
- 在四块全相联LRU影子下分类两配置的实际缺失;只改对象布局构造一个冲突消失的例子
解答一:先确定架构结果
I1把x2从旧5改为0,I2得到x3=0+9=9,I3把9写到0x04。I4把x4从旧1改为0,所以I5相等跳转。目标用分支自己的PC计算:0x1010+12=0x101c,即相对偏移0x10+12=0x1c,J1与J2都不应完成。
后续load应得到x5=0、x9=0、x10=9、x11=0、x12=13、x13=11。x6仍为9,x7仍为11,x8仍为13,x1仍为0。最后非零数据word只有0x04的9、0x08的13、0x40的11;例如地址0x04..0x07的字节为09 00 00 00。
I1至I13共13条有效指令,包括分支,不包括两条错误路径指令。按有效访存指令顺序,数据轨迹为:
| 次数 | 来源 | 操作 | 返回/写入的word |
|---|---|---|---|
| 1 | I1 | R 0x00 | 0 |
| 2 | I3 | W 0x04 | 9 |
| 3 | I4 | R 0x20 | 0 |
| 4 | I6 | R 0x00 | 0 |
| 5 | I7 | W 0x40 | 11 |
| 6 | I8 | R 0x20 | 0 |
| 7 | I9 | R 0x04 | 9 |
| 8 | I10 | W 0x08 | 13 |
| 9 | I11 | R 0x28 | 0 |
| 10 | I12 | R 0x08 | 13 |
| 11 | I13 | R 0x40 | 11 |
解答二:完整逐周期账本
IF栏表示本周期的取指尝试。发生前端保持时,该尝试不写入IF/ID,下一周期会重取同一PC;ID中的等待指令则确实保留。表中“—”为无效槽,未列出的寄存器和数据内存保持原值。所有有效MEM读写都列在最后一栏,所以可直接从表里提取上面的同一条trace。
| 周期 | IF | ID | EX | MEM | WB | 控制、转发与架构效果 |
|---|---|---|---|---|---|---|
| 1 | I1 | — | — | — | — | 正常推进 |
| 2 | I2 | I1 | — | — | — | 正常推进 |
| 3 | I3 | I2 | I1 | — | — | 保持PC/IF-ID;下一EX插气泡 |
| 4 | I3 | I2 | — | I1 | — | R 0x00 → 0 |
| 5 | I4 | I3 | I2 | — | I1 | MEM/WB→I2.rs1:x2=0;x2←0 |
| 6 | I5 | I4 | I3 | I2 | — | EX/MEM→I3.rs2:x3=9 |
| 7 | J1 | I5 | I4 | I3 | I2 | 保持PC/IF-ID;下一EX插气泡;x3←9;W 0x04 ← 9 |
| 8 | J1 | I5 | — | I4 | I3 | R 0x20 → 0 |
| 9 | J2 | J1 | I5 | — | I4 | MEM/WB→I5.rs1:x4=0;分支成立;清J1/J2,下一IF=I6;x4←0 |
| 10 | I6 | — | — | I5 | — | 正常推进 |
| 11 | I7 | I6 | — | — | I5 | 正常推进 |
| 12 | I8 | I7 | I6 | — | — | 正常推进 |
| 13 | I9 | I8 | I7 | I6 | — | R 0x00 → 0 |
| 14 | I10 | I9 | I8 | I7 | I6 | x5←0;W 0x40 ← 11 |
| 15 | I11 | I10 | I9 | I8 | I7 | R 0x20 → 0 |
| 16 | I12 | I11 | I10 | I9 | I8 | x9←0;R 0x04 → 9 |
| 17 | I13 | I12 | I11 | I10 | I9 | x10←9;W 0x08 ← 13 |
| 18 | — | I13 | I12 | I11 | I10 | R 0x28 → 0 |
| 19 | — | — | I13 | I12 | I11 | x11←0;R 0x08 → 13 |
| 20 | — | — | — | I13 | I12 | x12←13;R 0x40 → 11 |
| 21 | — | — | — | — | I13 | x13←11 |
两次load-use分别在周期3、7检出。周期4、8的EX为气泡,旧load继续MEM。周期5的I2从MEM/WB取I1的0;周期6的I3从EX/MEM取I2的9作为store数据;周期9的I5从MEM/WB取I4的0作分支比较。没有把load的地址误当数据,也没有把ID读到的旧x2/x4用于最终计算。
周期9只清年轻的J1/J2,I5继续流过MEM/WB;它不写寄存器,但仍是有效的已执行分支。最后I13在周期21写回11。从第一条IF到最后一条WB,共21个模型周期,可分账为13条有效指令+4个填充/排空周期+2次load-use空槽+2个taken分支空槽。这个等式依赖本例没有相互重叠的其他停顿,不能作为任意程序的罚时相加公式,更不是实际CPU预测。
解答三:同一trace的地址拆分
D:offset为地址[2:0],index为[4:3],tag为[31:5]。A:offset仍为[2:0],index改为[3],tag为[31:4]。下面tag、index、offset用十进制,地址用十六进制。way不是地址位,由命中或替换结果决定。
| 次数 | 操作地址 | D:tag/index/offset | A:tag/index/offset |
|---|---|---|---|
| 1 | R 0x00 | 0/0/0 | 0/0/0 |
| 2 | W 0x04 | 0/0/4 | 0/0/4 |
| 3 | R 0x20 | 1/0/0 | 2/0/0 |
| 4 | R 0x00 | 0/0/0 | 0/0/0 |
| 5 | W 0x40 | 2/0/0 | 4/0/0 |
| 6 | R 0x20 | 1/0/0 | 2/0/0 |
| 7 | R 0x04 | 0/0/4 | 0/0/4 |
| 8 | W 0x08 | 0/1/0 | 0/1/0 |
| 9 | R 0x28 | 1/1/0 | 2/1/0 |
| 10 | R 0x08 | 0/1/0 | 0/1/0 |
| 11 | R 0x40 | 2/0/0 | 4/0/0 |
命中、受害者和写回
victim一律写块基址;“脏”表示本次替换前必须回写。空行没有victim。即使是只读请求,也可以触发旧脏块回写。
| 次数 | D结果 | D victim | A结果及way | A victim |
|---|---|---|---|---|
| 1 | 缺失 | — | 缺失,way 0 | — |
| 2 | 命中 | — | 命中,way 0 | — |
| 3 | 缺失 | 0x00(脏,回写) | 缺失,way 1 | — |
| 4 | 缺失 | 0x20(干净) | 命中,way 0 | — |
| 5 | 缺失 | 0x00(干净) | 缺失,way 1 | 0x20(干净) |
| 6 | 缺失 | 0x40(脏,回写) | 缺失,way 0 | 0x00(脏,回写) |
| 7 | 缺失 | 0x20(干净) | 缺失,way 1 | 0x40(脏,回写) |
| 8 | 缺失 | — | 缺失,way 0 | — |
| 9 | 缺失 | 0x08(脏,回写) | 缺失,way 1 | — |
| 10 | 缺失 | 0x28(干净) | 命中,way 0 | — |
| 11 | 缺失 | 0x00(干净) | 缺失,way 0 | 0x20(干净) |
D的第3、6、9次访问分别回写块0x00、0x40、0x08。A的第6、7次访问分别回写块0x00、0x40;第6次是读0x20却先保存旧块0x00中的9,正好检验旧tag是否保留。
LRU和数据状态也要留下
以下是每次访问后A的组内块次序,均按最近→最久排列,星号表示dirty。这比仅列最终tag多保留了“下次该逐出谁”的依据。
| 次数 | A组0 | A组1 |
|---|---|---|
| 1 | 0x00 | 空 |
| 2 | 0x00* | 空 |
| 3 | 0x20,0x00* | 空 |
| 4 | 0x00*,0x20 | 空 |
| 5 | 0x40,0x00 | 空 |
| 6 | 0x20,0x40* | 空 |
| 7 | 0x00,0x20 | 空 |
| 8 | 0x00,0x20 | 0x08* |
| 9 | 0x00,0x20 | 0x28,0x08* |
| 10 | 0x00,0x20 | 0x08*,0x28 |
| 11 | 0x40,0x00 | 0x08*,0x28 |
最终有效缓存行如下,每个八字节块用两个小端word表示;未列的行无效。
| 配置 | 组/way | 块基址 | 两个word | dirty |
|---|---|---|---|---|
| D | 0/0 | 0x40 | (11, 0) | 0 |
| D | 1/0 | 0x08 | (13, 0) | 0 |
| A | 0/0 | 0x40 | (11, 0) | 0 |
| A | 0/1 | 0x00 | (0, 9) | 0 |
| A | 1/0 | 0x08 | (13, 0) | 1 |
| A | 1/1 | 0x28 | (0, 0) | 0 |
解答四:结束条件决定传输账单
| 口径 | D | A |
|---|---|---|
| 需求命中/缺失 | 1 / 10 | 3 / 8 |
| 整块填入 | 10 | 8 |
| 需求脏回写 | 3 | 2 |
| 需求结束后dirty块数 | 0 | 1 |
| 此时下层word 0x04 / 0x08 / 0x40 | 9 / 13 / 11 | 9 / 0 / 11 |
| 若要求终末flush,追加整块回写 | 0 | 1 |
| 含终末flush的总填入+回写块数 | 13 | 11 |
A在需求结束时仍能从本地脏块读到13,所以它和顺序执行结果一致;只是下层内存还没收到这次修改。终末flush后两者的下层都包含9、13、11。这个flush只传播到本模型下层,未给出掉电持久化承诺。
若每次查询费用H、填入费用F、脏回写费用W均串行且不可重叠,则需求账单为D:11H+10F+3W,A:11H+8F+2W;要求终末flush时A再加W。没有H/F/W或允许重叠的接口,就不应把块计数改写成纳秒或真实CPU耗时。平均访存时间把局部缺失率、脏回写条件概率和串行罚时逐项展开。
解答五:反例与可迁移的检查
漏互锁,错误不仅出现在一条add
若允许消费者在load的MEM周期使用旧寄存器值,I2会用旧x2=5得到14,I3把14写到0x04。I5又可能用旧x4=1误判不跳转,于是J1把0x00写成11,J2把x6改为99,后续x5读到11。一个“取消停顿”的改动同时破坏数据与控制语义。
这条变异特意选用旧值,不把尚未就绪的地址当数据。后者是另一种错误,也必须由转发选择的load屏蔽排除。
漏清除,正确目标仍不能挽回副作用
如果周期9重定向PC却保留已在途的J1/J2,它们仍会写0x00=11和x6=99。目标I6确实被取到了,最后结果仍然错误。检查“PC到达目标”不足以证明分支恢复正确。
用新tag回写会污染新请求
A第6次读0x20时,victim是旧块0x00,里面的word0x04为9。正确回写地址来自旧tag和组号。若先覆盖tag,再把旧八字节写到新块0x20,就会把9错误放入0x24并丢失0x04的修改;以后读0x04会得到旧0。不能只检查本次R0x20仍返回0就判定回写正确。
替换和3C分类
用同容量、四块全相联LRU影子处理每一次访问。D的10个实际缺失是5首次、4冲突、1容量;A的8个实际缺失是5首次、2冲突、1容量。最后一次重读0x40时影子也已逐出该块,所以归为容量;它不是一个只靠增加路数就必然消失的缺失。
另取只交替读取两个对象的逻辑程序。在D上,布局a=0x00、b=0x20产生 00,20,00,20,四次全缺失;把b移到0x28产生 00,28,00,28,前两次缺失、后两次命中。算法与对象访问次序不变,修复的是索引冲突。局部性与3C分类给出影子参考的完整口径,以及空间局部性的另一组对照。
正确性证据与适用范围
顺序解释器与逐级流水模拟分别计算寄存器和字节内存,逐条提取的11次数据操作完全相同。缓存模拟在每次R时核对该word值,并保存tag、index、offset、way、valid/dirty、LRU次序、victim和下层字节;终末flush单独计费。自动检查也覆盖负分支编码、补码扩展与溢出判据。
这些有限实验为上面的具体答案提供可复算证据,不证明所有RV32I实现,也不证明任意长度程序上的流水正确性。流水数据正确性依赖“最近较老写者已就绪或互锁等待”,控制正确性依赖“错误路径在副作用前失效”,缓存正确性依赖“每次丢弃唯一新副本前保存脏数据”。
多核进阶可沿从缓存写回到多核写权限继续。MSI协议提供三个核心的完整交接表与归纳不变量;伪共享终点再对同一六次写复算共行/隔行的6对2独占请求。协议另固定原子总线,不会从本页单核trace直接推出跨地址顺序或持久性。
参考资料
- RISC-V International,RV32I 2.1,固定文档版v20260120;五条指令的真实语义来源
- UC Berkeley CS61C,Data Hazards 与 Set-Associative Cache,访问于2026-10-08;流水与缓存背景
- 本文EX末分支、时序、存储端口、LRU并列规则、完整程序和全部数值均为显式教学模型,不是规范规定的微架构或硬件测量