Memory renaming

一、memory renaming 和值预测的核心区别

1)预测对象不同 传统值预测预测的是“这条 load 这次大概率会得到什么值”。论文回顾的 Lipasti & Shen 方案,就是根据该 load 过去读到的值来预测当前值。相对地,memory renaming 先预测“这条 load 的数据生产者是谁”,即它最可能对应哪一条 store;一旦识别出这个稳定的 store-load 配对,再从该 store 对应的 value file 中取出最近一次写入的值。论文明确说它“结合了 dependence prediction 和 value prediction”,并强调它与普通值预测的差别在于:这里是间接地做值预测。

2)为什么作者觉得 memory renaming 更靠谱 论文在第 3 页表 1 的分析里指出:连续两次执行中 load 读到“相同值”的比例,整数程序平均约 29%,浮点程序平均约 44%;但“同一条 load 的生产者 store 保持不变”的稳定性更高,因此作者认为store-load 依赖关系比“值本身”更稳定,更适合作为 speculation 的基础。随后他们的 dependence predictor 在实验里最高可正确识别 76% 的内存依赖,平均为 62%。

3)关键索引方式不同 值预测通常是“load PC → predicted value”。而 memory renaming 是“load PC → store-load cache → value-file index → speculative value”。也就是说,load 的 PC 先去索引一张依赖表,找到它最可能对应的 store/load 边,再通过这条边关联到 value file 中的值。

4)对地址计算的作用不同 普通值预测只是把值提前猜出来;而 memory renaming 的目标更激进:让 load 在有效地址还没算出来之前,就像寄存器读取那样尽早拿到值,从而把一部分“通过内存传递的数据”提升为“通过寄存器式基础设施传递的数据”。论文第一页和第四页都在强调这一点。

5)置信机制不同 论文中特别指出,值预测的置信度通常基于“这条 load 自己过去的预测历史”;而 memory renaming 更看重“这条 store-load 配对关系是否稳定”。作者认为用 producer/consumer 关系做 confidence mechanism 更合适。

二、这篇文章里的 memory renaming 是怎么实现的

按论文的方案,可以把实现拆成下面几步。

1)新增硬件结构 最核心的是两张表:

store-load cache:记录某条 load 或 store 对应的依赖边,以及这条边关联的 value-file index。 value file:保存“最近一次写入某条依赖边的值”,供后续 load 直接读取。

此外还需要:

load 的 confidence counter; 在 ROB / LSQ / 保留站中增加若干字段,用来携带 value-file index、load/store 地址、fault 信息等; 可选地,把 value-file index 随 store 数据一起传播到 cache/memory hierarchy,用于后续学习新的 store-load 绑定。图 2 画出了这些流水线扩展。

2)Decode 阶段:建立或查找映射 在 decode 阶段,同时对 load 和 store 查询 store-load cache。

如果命中,就拿到这条依赖边对应的 value-file index,传给 rename 阶段。 如果没命中,就给这条指令在 store-load cache 分配表项,并同时在 value file 中分配一个条目,把索引写回 store-load cache。

论文还专门提到:即使是 load,也给它分配 value-file entry 是有益的,因为这样连常量或很少被 store 更新的变量也能享受更快的通信路径。decode 阶段还维护 load 的 confidence counter,预测对了加计数,错了减计数或清零。

3)Rename 阶段:像寄存器那样“读值” load 在 rename 阶段用刚才拿到的 value-file index 去访问 value file:

如果 value file 里已经有值,就直接返回这个 speculative value; 如果这个值还在飞行中,还没最终产生,就返回产生它的 reservation station / LSQ 标识,让 load 等待对应 store 的数据写回。

一旦 renamed load 获得该值,它就可以像普通寄存器生产者一样,把结果广播给后续依赖指令。也就是说,后续调度器并不需要大改,关键只是把“内存通信”提升成了“寄存器式通信”。

4)执行阶段:真实内存访问照常进行,用来校验预测 虽然 speculative value 已经提前被用了,但 load 仍然要正常做地址计算、访问 D-cache / memory。当真正从内存系统返回值后,把它和 speculative value 比较:

相同,则说明本次 memory renaming 成功; 不同,则发生 load data mis-speculation,需要恢复。

5)Store 的处理:在 retirement 时更新 value file 论文里 store 不会像 load 一样在前面就访问 value file,而是等到 retirement 时:

一边把 store 数据写入内存系统; 一边把该值写入 value file。

这样,后续匹配到这条依赖边的 load 就能直接从 value file 中得到值。作者还说明,他们不强求 value file 与主存保持一致;即使因为 DMA 等原因不一致,最终也会在 load 的真实内存访问校验阶段发现。

6)如何学习新的 store-load 配对关系 论文提出了两种学习新依赖边的方法:

简单方案:当 renamed store 通过 LSQ forwarding 网络把数据转发给某个 load 时,就据此更新该 load 在 store-load cache 中的表项; 更强方案:给 renamed store 的数据附带 value-file index,并把这个索引传播到 memory hierarchy。这样当一个原本未重命名的 load 读到这份数据时,就能反向学到自己的 producer 是谁。论文说这种方案效果更好,但开销更高。

7)错误恢复:squash 或 re-execution 论文研究了两种恢复方式:

squash recovery:像分支预测失败一样,把错误 load 之后的指令全扔掉,简单但代价大; re-execution recovery:把正确值重新注入结果总线,只让真正依赖这个错误值的指令重执行,复杂但便宜得多。

作者实验发现,re-execution 的代价明显更低,平均只消耗不到 squash 所需执行带宽的 1/3,因此整体效果更好。

三、如果把这篇文章的方法整理成一个可落地的实现方案

你可以把它理解成下面这个最小闭环:

预测器侧 用 LDPC 查询 store-load cache,得到 value_file_idx 和 confidence。 前端决策 只有当 confidence 达标时,才允许该 load 做 memory renaming;否则走普通 load 流程。论文也确实用了 confidence counters 来控制是否投机。 rename/read 阶段 用 value_file_idx 访问 value file: 若 value ready,直接把值送给该 load; 若 producer store 还在飞行中,返回其 reservation-station / LSQ tag,让该 load 等待。 execute/verify 阶段 load 继续正常算地址、查 cache、从真实内存取值;返回后与 speculative value 比较。 commit/train 阶段 store retirement 时更新 value file;load/store 实际发生 forwarding 或命中带 tag 的 cache line 时,反向训练 store-load cache。 recovery 优先用 re-execution,不建议简单全 squash,因为论文实验表明后者经常会吞掉重命名带来的收益。

四、这篇文章给出的结论

论文最后的总结是:通过把 value predictor、dependence predictor 和 value file 结合起来,让 load 更早执行,平均可以带来 16% 的应用加速;在某些配置下,执行时间改善最高可达 41%,而且这种收益不只体现在栈和全局数据上,对 heap 访问也有帮助。