微处理器设计/寄存器重命名
在寄存器机器中,程序由操作值的指令组成。指令必须命名这些值,以便将它们彼此区分。一个典型的指令可能会说,“将 X 和 Y 相加,并将结果放入 Z 中”。在这个指令中,X、Y 和 Z 是存储位置的名称。
为了获得紧凑的指令编码,大多数处理器指令集都有一组小的专用位置,可以直接命名。例如,x86 指令集体系结构有 8 个整数寄存器,x86-64 指令集体系结构有 16 个,许多 RISC 微处理器有 32 个,而 IA-64 指令集体系结构有 128 个。在较小的处理器中,这些位置的名称直接对应于寄存器文件的元素。
不同的指令可能需要不同的时间;例如,处理器可能在单个主内存加载操作正在进行时执行数百条指令。在加载操作完成之前执行的较短指令将先完成,因此指令将按与原始程序顺序不同的顺序完成。大多数最近的高性能 CPU 中都使用乱序执行来实现其部分速度提升。
在可能的情况下,汇编语言编译器会尝试将不同的指令分配给不同的寄存器。但是,由特定微处理器体系结构限制,汇编代码中可以使用有限的寄存器名称。许多高性能 CPU 拥有比指令集中可以直接命名的物理寄存器更多的物理寄存器,因此它们在硬件中重命名寄存器以实现额外的并行性。
当多条指令引用特定位置作为操作数时,无论是读取(作为输入)还是写入(作为输出),以与原始程序顺序不同的顺序执行这些指令会导致三种数据冒险。
- 读后写 (RAW) 错误
- 从寄存器或内存位置读取必须返回程序顺序中最后一个写入的值,而不是其他写入的值。这被称为真依赖或流依赖,需要指令按程序顺序执行。
- 写后写 (WAW) 错误
- 对特定寄存器或内存位置的连续写入必须使该位置包含第二次写入的结果。如果需要,可以通过压缩(同义词:取消、废除、 moot)第一次写入来解决这个问题。WAW 依赖项也被称为输出依赖项。
- 写后读 (WAR) 错误
- 从寄存器或内存位置读取必须返回写入该位置的最后一个先前值,而不是在读取之后程序上写入的值。这是一种错误依赖,可以通过寄存器重命名来解决。WAR 依赖项也被称为反依赖。
与其延迟写入直到所有读取完成,不如维护该位置的两个副本:旧值和新值。如果读取在程序顺序中领先,则可以向新值的写入提供旧值,即使其他在写入之后的读取被提供新值。错误依赖被打破,并且创造了更多进行乱序执行的机会。当所有需要旧值的读取都已满足时,可以丢弃它。这就是寄存器重命名的基本概念。
任何被读取和写入的东西都可以被重命名。虽然最常讨论的是通用寄存器和浮点寄存器,但标志寄存器和状态寄存器,甚至单个状态位,也通常被重命名。内存位置也可以被重命名,虽然它不像寄存器重命名那样普遍。一个例子是 Transmeta Crusoe 处理器的门控存储缓冲区,这是一种内存重命名的形式。
如果程序避免立即重用寄存器,则无需寄存器重命名。一些指令集(例如,IA-64)专门为此目的指定了非常大量的寄存器。但是,这种方法有其局限性。
- 编译器很难在不大幅增加代码大小的情况下避免重用寄存器。例如,在循环中,连续迭代必须使用不同的寄存器,这需要在一个称为循环展开(但请参阅寄存器旋转)的过程中复制代码。
- 大量的寄存器需要更多位来指定指令中作为操作数的寄存器,从而导致代码大小增加。
- 许多指令集在历史上指定了较少的寄存器数量,现在无法更改。
代码大小增加很重要,因为当程序代码更大时,指令缓存未命中次数会更多,处理器会等待新指令而停顿。
机器语言程序指定对指令集体系结构 (ISA) 指定的有限的寄存器集进行读写。例如,DEC Alpha ISA 指定了 32 个整数寄存器,每个寄存器 64 位宽,以及 32 个浮点寄存器,每个寄存器 64 位宽。这些是架构寄存器。为运行 Alpha 指令集的处理器编写的程序将指定操作,读取和写入这 64 个寄存器。如果程序员在调试器中停止程序,他们可以观察这 64 个寄存器的内容(以及一些状态寄存器)以确定机器的执行进度。
实现此 ISA 的一个特定处理器,Alpha 21264,具有 80 个整数和 72 个浮点物理寄存器。在 Alpha 21264 芯片上,有 80 个物理独立的位置可以存储整数运算的结果,以及 72 个位置可以存储浮点运算的结果。[1]
以下文本描述了两种类型的寄存器重命名,它们的区别在于保存数据以备执行单元使用的电路。
在所有重命名方案中,机器将指令流中引用的架构寄存器转换为标签。当架构寄存器可能由 3 到 5 位指定时,标签通常是 6 到 8 位的数字。重命名文件必须为每个周期重命名的每个指令的每个输入提供一个读取端口,以及为每个周期重命名的每个指令的每个输出提供一个写入端口。由于寄存器文件的大小通常随端口数量的平方而增长,因此重命名文件通常在物理上很大,并且会消耗大量的功率。
在标签索引寄存器文件风格中,有一个大型的用于数据值的寄存器文件,包含一个寄存器,对应于每个标签。例如,如果机器有 80 个物理寄存器,那么它将使用 7 位标签。在这种情况下,48 个可能的标签值是未使用的。
在这种风格中,当将指令发出到执行单元时,源寄存器的标签被发送到物理寄存器文件,在那里,对应于这些标签的值被读取并发送到执行单元。
在预约站风格中,有很多小的关联寄存器文件,通常在每个执行单元的输入处都有一个。问题队列中每条指令的每个操作数在这些寄存器文件之一中都有一个存放值的位置。
在这种风格中,当将指令发出到执行单元时,对应于问题队列条目的寄存器文件条目被读取并转发到执行单元。
- 架构寄存器文件或退休寄存器文件 (RRF)
- 机器的已提交寄存器状态。按逻辑寄存器编号索引的 RAM。通常在结果从重排序缓冲区退休或提交时写入。
- 未来文件
- 机器最推测的寄存器状态。按逻辑寄存器编号索引的 RAM。
- 活动寄存器文件
- 英特尔 P6 组对未来文件的术语。
- 历史缓冲区
- 通常与未来文件结合使用。包含已覆盖寄存器的“旧”值。如果生产者仍在运行,它可能是按历史缓冲区编号索引的 RAM。在分支预测错误后,必须使用历史缓冲区中的结果——要么复制它们,要么禁用未来文件查找,并通过逻辑寄存器编号以“内容寻址内存”(CAM)的形式索引历史缓冲区。
- 重排序缓冲区 (ROB)
- 一个根据每个操作依次(循环)索引的结构,用于正在执行的指令。它与历史缓冲区不同,因为重排序缓冲区通常位于未来文件(如果存在)之后,并位于体系结构寄存器文件之前。
重排序缓冲区可以是无数据的或有数据的。
在 Willamette 的 ROB 中,ROB 项指向物理寄存器文件 (PRF) 中的寄存器,还包含其他簿记信息。这也是 Andy Glew 在伊利诺伊州与 HaRRM 一起完成的第一个乱序设计。
在 P6 的 ROB 中,ROB 项包含数据;没有单独的 PRF。ROB 中的数据值在退役时从 ROB 复制到 RRF。
但是,有一个小细节:如果 ROB 项中存在时间局部性(即,如果冯·诺依曼指令序列中紧密相邻的指令在时间上紧密相邻地写回,则可能在 ROB 项上执行写组合,因此与单独的 ROB/PRF 相比,端口更少)。目前尚不清楚这是否会造成影响,因为 PRF 应该是银行化的。
ROB 通常没有关联逻辑,当然 Andy Glew 设计的任何 ROB 都没有 CAM(见上文)。然而,设计师 Keith Diefendorff 多年来一直坚持认为 ROB 具有复杂的关联逻辑。另一方面,第一个 ROB 提议可能利用了 CAM。
在重命名阶段,每个引用的体系结构寄存器(用于读或写)都在体系结构索引的重映射文件中查找。该文件返回一个标签和一个就绪位。如果有一个排队的指令将写入该寄存器,但尚未执行,则该标签为非就绪状态。对于读操作数,此标签在指令中代替体系结构寄存器。对于每个寄存器写入,从一个空闲标签 FIFO 中提取一个新标签,并将一个新的映射写入重映射文件,以便将来读取体系结构寄存器的指令将引用此新标签。该标签被标记为非就绪状态,因为该指令尚未执行。为该体系结构寄存器分配的先前物理寄存器与指令一起保存在重排序缓冲区中,重排序缓冲区是一个 FIFO,用于在解码和毕业阶段按程序顺序保存指令。
然后将指令放置在各种发布队列中。当指令执行时,其结果的标签会被广播,发布队列将这些标签与非就绪源操作数的标签进行匹配。匹配意味着操作数已就绪。重映射文件也匹配这些标签,以便它可以将相应的物理寄存器标记为就绪状态。当发布队列中指令的所有操作数都就绪时,该指令就可以发布了。发布队列选择就绪指令在每个周期发送到各种功能单元。非就绪指令保留在发布队列中。从发布队列中取消排序的指令可能会使它们变大且功耗高。
已发布的指令从标签索引的物理寄存器文件(绕过刚刚广播的操作数)中读取,然后执行。执行结果被写入标签索引的物理寄存器文件,以及广播到每个功能单元前面的旁路网络。毕业将先前写入的体系结构寄存器的标签放入空闲队列,以便可以将其重新用于新解码的指令。
异常或分支预测错误会导致重映射文件通过状态快照和在按顺序的毕业前队列中循环遍历先前标签的组合,备份到最后一个有效指令的重映射状态。由于需要这种机制,并且由于它可以恢复任何重映射状态(不仅仅是当前正在毕业的指令之前的状态),因此可以在分支到达毕业之前处理分支预测错误,从而有可能隐藏分支预测错误的延迟。这是 MIPS R10000、Alpha 21264 和 AMD Athlon 的 FP 部分中使用的重命名样式。
在重命名阶段,每个用于读取的体系结构寄存器都在未来文件和重命名文件两者中查找。未来文件读取提供了该寄存器的值,如果还没有未完成的指令写入它(即,它已就绪)。当指令被放置在发布队列中时,从未来文件读取的值被写入保留站中相应的条目。指令中的寄存器写入会导致一个新的非就绪标签被写入重命名文件。标签号通常按指令顺序依次分配——不需要空闲标签 FIFO。
与标签索引方案一样,发布队列等待非就绪操作数查看匹配的标签广播。与标签索引方案不同,匹配的标签会导致相应的广播值被写入发布队列条目的保留站。
已发布的指令从保留站读取其参数,绕过刚刚广播的操作数,然后执行。如前所述,保留站寄存器文件通常很小,可能只有八个条目。
执行结果被写入重排序缓冲区,写入保留站(如果发布队列条目具有匹配的标签),以及写入未来文件,如果这是最后一个针对该体系结构寄存器的指令(在这种情况下,寄存器被标记为就绪)。
毕业将值从重排序缓冲区复制到体系结构寄存器文件。体系结构寄存器文件的唯一用途是从异常和分支预测错误中恢复。
在毕业时识别到的异常和分支预测错误会导致体系结构文件被复制到未来文件,以及所有在重命名文件中标记为就绪的寄存器。通常没有办法重建某个指令(解码和毕业之间的某个指令)的未来文件的状态,因此通常没有办法尽早从分支预测错误中恢复。这是 AMD K7 和 K8 设计的整数部分中使用的样式。
在这两种方案中,指令按顺序插入发布队列,但按乱序删除。如果队列不折叠空槽,那么它们将要么具有许多未使用的条目,要么需要某种可变优先级编码,用于同时准备就绪的多条指令。折叠空洞的队列具有更简单的优先级编码,但需要简单但大量的电路来推动指令通过队列。
保留站从重命名到执行具有更好的延迟,因为重命名阶段直接找到寄存器值,而不是找到物理寄存器号,然后使用它来找到值。这种延迟表现为分支预测错误延迟的组成部分。
保留站从指令发布到执行也具有更好的延迟,因为每个本地寄存器文件都比标签索引方案的大型中央文件更小。标签生成和异常处理在保留站方案中也更简单,如下所述。
保留站使用的物理寄存器文件通常与它们服务的发布队列并行折叠未使用的条目,这使得这些寄存器文件在总量上更大,功耗更高,并且比标签索引方案中使用的更简单的寄存器文件更复杂。更糟糕的是,每个保留站中的每个条目都可以由每个结果总线写入,因此具有每个功能单元 8 个发布队列条目的保留站机器通常具有 9 倍于等效标签索引机器的旁路网络数量。因此,结果转发消耗的功耗和面积远高于标签索引设计。
此外,保留站方案有四个地方(未来文件、保留站、重排序缓冲区和体系结构文件)可以存储结果值,而标签索引方案只有一个(物理寄存器文件)。由于从功能单元广播到所有这些存储位置的结果必须到达机器中比标签索引方案更多的位置,因此此功能消耗的功耗、面积和时间更多。尽管如此,在配备了非常准确的分支预测方案且执行延迟是主要关注点的机器中,保留站可以工作得非常好。
- ↑ 实际上,还有更多位置,但这些额外位置与寄存器重命名操作无关。)