超标量重命名
需要额外检查同一周期内指令之间的 RAW 依赖
CS152 · 第 13 课
这一课把第 12 课的重命名机制从"每周期一条指令"扩展到"每周期多条指令",然后转向乱序执行里最容易被忽视、却往往是继控制冒险之后第二大瓶颈的问题:访存指令能不能也乱序执行?
把重命名逻辑扩展到每周期重命名 N 条指令,只需要给重命名表和空闲寄存器列表增加额外的读写端口——但这样"能直接工作吗"?答案是:还必须额外检查同一周期内发射的指令之间的 RAW 冒险——例如第 2 条指令的源寄存器恰好是第 1 条指令要写的目的寄存器。解决办法是用"=?"比较器与重命名表查找并行进行:如果匹配,就用旁路多路选择器把第 1 条指令刚分配的标签直接转发给第 2 条指令,而不是让它去查(此时还未更新的)重命名表。真实例子:MIPS R10K 每周期可以重命名 4 条彼此存在 RAW 依赖的指令。
课程明确指出:继控制冒险之后,访存数据冒险是限制超标量性能的第二大瓶颈。现代超标量处理器普遍采用精细的 load/store 重排序机制,通过推测性地提前发射 load 来降低有效访存延迟。
推测性存储缓冲区(Speculative Store Buffer):store 在提交之前绝不能真正修改内存;一个推测性存储缓冲区持有尚未提交的 store 地址/数据(字段 V、S[推测位]、Tag、Data);store 被拆成"store address"和"store data"两个微操作(地址微操作写 Tag 字段,数据微操作写 Data 字段)。Store 提交的条件是:它是程序顺序中最旧的未提交指令,且地址和数据都已就绪——此时清除推测位,数据最终写入 L1 数据缓存。Store 中止(分支预测错误或异常回滚时)只需清除该缓冲区条目的有效位。
Load 如何从推测性存储缓冲区"旁路"取数:两个具体设计问题都有明确答案——如果同一个地址的数据同时存在于存储缓冲区和数据缓存中,应该使用存储缓冲区里的值(而不是缓存里可能过时的旧值);如果存储缓冲区里对同一地址有多个待处理的 store,应该使用程序顺序上最年轻、但仍然早于该 load 的那一个 store。
以 sd x1,(x2) 之后紧跟 ld x3,(x4) 为例:
x4 != x2 时,load 才可以先于更早的 store 执行;每个 load 地址要和所有更早、尚未提交的 store 地址比较(可以只比较低若干位地址以节省硬件,作为一种保守近似);只要有任何更早 store 的地址还未知,load 就不能执行。真实例子:MIPS R10K,16 项地址队列。x4 != x2,在 store 地址甚至还没算出来之前就提前执行 load——需要把所有已完成但未提交的 load/store 地址按程序顺序保留下来;如果之后发现 x4==x2,就必须撤销该 load 及其之后的所有指令。地址推测出错的代价很大。内存依赖预测(真实例子:Alpha 21264):在地址推测出错之后,把那条具体的(静态)load 指令标记为"store-wait";此后这条指令的每一次动态执行都会保守地等待所有更早的 store 完成;这个标记会被周期性清除,以便在冒险只是暂时性的情况下重新尝试推测。
问题 1:"保守式乱序 load 执行"和"地址推测"这两种策略的关键区别是什么?
问题 2:如果同一个地址的数据同时存在于推测性存储缓冲区和数据缓存中,load 应该使用哪一份数据?
问题 3:Alpha 21264 的"store-wait"位机制具体解决了什么问题?
需要额外检查同一周期内指令之间的 RAW 依赖
store 拆成地址/数据两个微操作,最旧且就绪才提交
顺序队列 → 保守乱序(地址已知不等)→ 地址推测(赌一把)
Alpha 21264 的 store-wait 位,从错误中学习
sp23/lecture_slides/L13_OoO_Execution_II.pdf(UC Berkeley CS152/CS252A, Spring 2023, Sophia Shao)