虽然早在 1967 年就已经提出了 Tomasulo 调度算法 [1],但网上仍很少找到关于落到模块粒度的教程文档。从零复现一遍成本太大,因此用画原理图的方式做思想实验,尝试理解 Tomasulo 在电路上如何实现。
处理数据
首先明细几个概念
- 指令(Instruction):包含指令类型(ADD、MUL、LD、ST 等),输入:输入寄存器地址(Src Addr) 或是 立即数,目标寄存器地址(Dst Addr)。
- 当前指令(This Operator)以及先前指令(Operator)。标记指令的字段,可以唯一追踪到程序中的某条指令,一种方法是用在指令缓存中的地址表示。
- 保留站(Reservation Station)的输入:包含五个字段,当前指令、依赖指令1、依赖指令2、数据1、数据2。
寄存器要重命名,可为何指令用寄存器?
原始指令中用寄存器建立图依赖关系,而寄存器建立的依赖关系并非真正的依赖关系。
比如:
- 指令 1 -> 输出寄存器 A
- 指令 2 -> 输出寄存器 A
- 指令 3 <- 输入寄存器 A
可见指令3实际上依赖的是指令2的值,和指令1输出的值无关,但指令1和指令2输出共用了同一个寄存器,有可能因为伪依赖导致不能乱序执行。指令采用寄存器的方式传递指令间的信息本质时源于这符合物理描述,寄存器很容易映射到物理存储,但违背了逻辑描述。
逻辑上指令间的依赖关系用图表示,图依赖是建立指令之间的依赖(或者数据上的依赖),而非物理映射的存储地址。因此乱序思路是用指令信息表征依赖关系替代寄存器名称,这种方法叫做寄存器重命名。
实现分析
这是不带 Speculative Execution 的 Tomasulo 调度硬件原理图。
Register (实现寄存器重命名功能)
寄存器重命名的实现有两个基础:
- 程序正确执行的判据: 乱序执行只需要保证程序执行完的状态量一致(各层 DRAM、Cache 、Register File 内的值),而中间状态变更的过程可以任意调整;
- 指令序列是因果序列: 后端执行的指令通过前端的 Instruction Queue 传入,Queue 表示程序是序列,同时是因果的,任意指令的输入寄存器都是由之前指令的输出寄存器所写入。或者说从依赖图转到序列的过程满足拓扑排序。
只要在从 Instruction Queue 读入指令时,用表示指令的字段替换掉寄存器存下来,而由于指令是因果的,当输入新指令时其的依赖指令一定已经存储下来了。可以说 Register 存储了 寄存器 -> 指令的 映射(地址->指令内容),使用一个额外空间存储追踪这种映射关系。
图中红色绿色表示这个 entry 的值是否有效,实际上只要保证 2 ,新输入的指令所读取的寄存器一定是以及保存过的,这个标志去掉也没问题。
Reservation Station
指令是序列输入的,而依赖是图构成的,序列一定会将一些没有依赖的节点排在前面,为了让不被前面的节点阻塞流水线。需要一个存储记录住还没执行的指令,并时刻检测一但条件满足立刻执行,这便是 Reservation Station。
如图是程序依赖关系,每个节点是一条指令,节点的数字代表被塞入 Instruction Queue 的顺序,灰色代表已经执行的指令,蓝色是正在执行,红色是在 RS 里。当前正在时刻 4,此时 2 和 3 号程序在站点里休息等待 1 执行完毕。
从以上分析可知,需要一个物理存储让指令们休息,还需要一个判断机制让指令们离开。通过保存前级 register 映射的指令信息并通过监听数据总线(Common Data Bus)获取当前完成的指令。
另外,由于后继资源可能阻塞(比如多个执行单元对总线资源的占用),即使执行条件就绪也不一定就能立刻执行,因此还需要一个缓存记录可以执行的指令,并通过一个调度策略从中选择指令执行。
Others
执行单元不必多说。系统中同时有多个 RS 用于给不同种类指令休息,每个 RS 中保存的指令不重叠,没有 coherence 问题,但依赖关系应该是广播的,所以通过 CMB 广播消息。同时 CMB 也解耦了输入数据和寄存器堆,类似引入了旁路,但 RS 和寄存器堆内也可能对一个数据重复更新。
综上,整个 Tomasulo 算法可以概括完成信息的映射过程:
寄存器 --(Register)--> 指令 --(Reservation Station)--> 数据 -- (执行单元) --> 数据
https://en.wikipedia.org/wiki/Tomasulo's_algorithm ↩︎