这是本节的多页打印视图。 点击此处打印.

返回本页常规视图.

分支预测器与昆明湖微架构实现

在处理器微架构中,什么是分支预测?它为何重要?如何对其进行功能、性能验证?通过本期的开放验证学习,你将对高性能昆明湖微架构中的BPU(Branch Prediction Unit)部分有深入的了解

1 - 香山分支预测单元(BPU)基础设计

本文档介绍了香山分支预测单元的基础设计思想,通过阅读本文档,你不需要得知具体的信号名称和代码细节便可理解香山分支预测单元的大致工作流程。

在处理器设计中,一个设计优秀的分支预测器(BPU)是提高处理器性能的关键部件,它负责指导处理器的取指,确定处理器下一步要到哪里取出指令并执行。BPU 是一条指令生命周期最开始的地方,因此想要深入探究一个高性能处理器,从BPU开始是一个很好的选择。

在香山中亦是如此,香山作为一个乱序六发射的高性能处理器,自然需要一个准确度高、预测效率高的分支预测单元。而一个分支预测单元的设计实际中往往需要考虑很多因素,例如时序、结构的复杂度、占用的硅片面积、预测的准确度、预测错误时恢复的速度等等。香山处理器的分支预测单元在这些因素之间都做到了比较好的权衡,通过许多巧妙的设计使其拥有了很高的分支预测效率与准确度,为后端的指令供给提供了基础的保障。

在本节中,我们将会介绍香山预测单元的基础设计,通过阅读本节,可以掌握如下知识:

  • 分支预测的基本概念
  • 香山分支预测单元的基本预测单位——分支预测块
  • 香山分支预测单元的对外交互接口
  • 香山分支预测单元的基本结构
  • 香山分支预测单元的基础时序

任何参与BPU众包验证工作的同学,都需要率先阅读本节,以便对香山分支预测单元有基础了解。

1.1 - 何为分支预测

分支预测单元,顾名思义需要完成一件基本的任务——分支预测。在深入探讨分支预测单元之前,必须要了解分支预测是什么。

为何需要分支预测?

分支预测主要有两方面的原因:一是程序的执行流中含有分支指令,二是高性能处理器使用流水线设计

程序的执行流中含有分支指令

int x = 10;
int y = 20;
int result = 0;

if (x >= y) {
    result = x + y;
} else {
    result = x - y;
}

上述是一段C语言代码,这段代码首先定义了三个变量 x, y 和 result,然后根据 x 和 y 值的大小情况对result进行赋值。可以发现,程序在前三行对变量进行赋值时是顺序往下执行的,而在第 5 行时,由于 if 指令的出现,程序产生了分支,从第 5 行直接跳转到了第 8 行继续运行,这就造成了程序执行的分支

翻译成 RISC-V 汇编之后的代码如下:

li  a0, 10               # x = 10
li  a1, 20               # y = 20
li  a2, 0                # result = 0

blt a0, a1, else_branch  # 如果 x < y,则跳转到 else_branch
add a2, a0, a1           # 否则执行 result = x + y
j end                    # 跳转到 end
else_branch:
sub a2, a0, a1           # result = x - y
end:

可以发现程序依然保持着先前的分支行为,在代码的前三行,指令顺序执行,之后,在程序的第 5 行,出现了一条特殊指令blt ,我们称之为分支指令,它会根据 x 和 y 的大小关系决定指令流顺序往下执行还是跳转到其他地方,该指令的出现导致程序的执行出现了分支。

高性能处理器使用流水线设计

因此有了分支预测的概念,如果我们可以在执行结果产生之前,精准的预测出下一条指令的地址,便可以使处理器一直高效的运行下去。

分支预测的可行性

为什么可以进行分支预测呢?我们看接下来一个例子

if (data >= 128)
    sum += data;

假设该条指令将被重复执行,并且 data 从 0 开始递增,即 data = 0, 1, 2, 3 … 128, 129…,接下来我们可以分析每次执行这条指令的行为。

T = branch taken
N = branch not taken

data   = 0, 1, 2, 3, s, ... 126, 127, 128, 129, 130, ... 250, 251, 252, ...
branch = N  N  N  N  N  ...   N    N    T    T    T  ...   T    T    T  ...

       = NNNNNNNNNNNN ... NNNNNNNTTTTTTTTT ... TTTTTTTTTT  (easy to predict)

可以看出,在前 128 次,该分支都是 Not Taken (条件不成立)的,但在 128 次之后,分支一直都是 Taken (条件成立)的。如果我们用最简单的方法根据上一次是否 Taken,来预测本次是否 Taken,在整个预测过程中,我们只会产生一次预测错误。

上述现象的产生是源于一个基本事实的——分支指令是否跳转与该条指令以往的跳转行为有关联,我们通过总结以往跳转的规律,便可以对本次跳转产生较为精准的预测,这也使得分支预测变得可能。

事实上,分支指令的跳转还会与其他分支指令的跳转情况等因素相关,充分发掘有效信息,产生精准预测结果,是分支预测的主要任务之一。

分支预测的基本类型

在RISC-V 中,分支指令包含两种类型:

  1. 条件分支指令 (beq, bne, blt, bge, bltu, bgeu) 对于这类指令,是否跳转时候其中的条件决定,跳转目标可以在指令中获取,因此我们需要预测该指令是否跳转
  2. 无条件跳转指令 (jal, jalr) 对于这类指令,每当执行到它时是总是跳转,但跳转目标有可能由寄存器执行,因此我们需要预测该指令的跳转地址。

幸运的是,由于RISC-V架构设计的简洁,我们不需要处理条件跳转指令,每一个需要我们预测的跳转指令都是无条件的,这也给我们的设计提供了便利。

上述分析中,我们可以总结出分支预测的两大基本类型——方向预测目标地址预测

分支指令的方向预测

分支指令的方向预测对应到 RISC-V 指令就是条件分支指令,我们需要预测它是否需要跳转,也就是分支的方向,因此叫做方向预测。

两位饱和计数器

方向预测有一种非常简单并且十分高效的预测方法,叫做叫做饱和计数器法,它的大致思想可以参考下图。

两位饱和计数器被视作一个状态机,我们为每一个分支指令都维护这样一个状态机器,当一条分支指令发生跳转时,对应上图中的状态向右移动,否则状态向左移动。这样在下次遇到这条分支指令时,我们首先查找到它的两位饱和计数器,如果状态更偏右则预测其跳转,否则预测其不跳转。

当然,为每一条分支指令都维护一个两位饱和计数器是不现实的,因此在实际中,我们通常会采取PC部分位,或哈希的方法来索引两位饱和计数器,如下图所示。

分支历史

分支历史是方向预测中非常常用的数据,也是大多分支预测算法的基础,因为分支历史是指令以往跳转行为最直接的展示。

分支历史有以下两种基本类型:

  • 局部分支历史 为每一条分支指令维护一组寄存器,记录该条指令的历史跳转情况
    • 例如: 0101000000101 (0代表不跳转,1代表跳转)
  • 全局分支历史 所有指令共用一组寄存器,记录程序执行过程中的分支跳转情况
    • 例如:

          beg a0, a1, label1          不跳转  记录0
          bne a1, a2, label2          不跳转  记录0
      ->  beq a2, a3, label4          跳转    记录1
      

      ​执行完三条不同的分支指令后,全局分支历史变为 001

分支指令的目标地址预测

RISC-V架构中,分支指令的目标地址预测是指对无条件跳转指令(如jal、jalr)的跳转目标地址进行预测。由于这类指令总是执行跳转操作,我们需要预测其跳转的目标地址。

分支目标缓存(BTB)

BTB 是目标地址预测的一种常用方法,它的核心思想是使用一个缓存来存储以往无条件跳转指令的跳转目标,之后如果再次执行到这一条无条件跳转指令,就可以查看BTB中是存在该指令的记录,将记录的跳转目标作为本次预测的跳转目标。

示意图如下

预测指令类型

我们已经知道,在分支预测中,对于条件分支指令来说,我们需要预测其方向,对于无条件跳转指令来说,我们需要预测其跳转目标。但是出现了一个问题,拿到一个需要预测的PC之后,我们并不清楚这个PC所对应的指令是什么,也就是说我们根本不知道当前指令到底是一条普通指令还是一条分支指令,也就无法进行预测了。

如何解决呢?可以采取的一种方法是,在取出指令之后,对这条指令的行为进行预测。但取指需要从 ICache 或 Memory 中去取,有可能会耗费多个周期,这是这种方法的一大弊端。

更好的一种方法是,直接预测指令的类型,拿到一个PC之后,可以直接预测出这条指令是否是分支指令,并对指令行为进行预测。这样一来,我们就没有必要等待取指完成,并且预测出来的结果还可以指导 CPU 到什么地方去取指。

类型预测的方法,可以与BTB相似,在缓存中的某个字段中加入指令的类型,以供下一次预测使用。

分支预测的一般步骤

通过本节知识的介绍,我们可以总结出分支预测的一般步骤

  1. 获取 PC
  2. 预测是否是分支指令
    1. 如果是条件分支指令,预测其跳转方向和跳转目标
    2. 如果是无条件跳转指令 ,预测其跳转目标

注意,由于在预测中需要预测指令类型,没有获取到指令具体内容,所以对于条件分支指令来说,预测它的跳转目标也变成了我们的工作。

1.2 - 香山分支预测单元基础

本节介绍了香山分支预测单元(BPU)的基础思想和工作原理,包括采用的分支预测块思想、多预测器、多流水线结构以及取指目标队列(FTQ)的作用,说明了 BPU 对外交互的主要接口。

分支预测块思想

对于一般的分支预测器来说,通常会根据给定的 PC 预测出该 PC 所对应的指令的相关信息,如是否是条件分支指令、是否是跳转指令。对于条件分支指令,会预测出是否会跳转,而对于跳转指令,则会预测出跳转目标。然而,这种一条一条地预测指令的方法效率较低,导致前端指令供给过慢。

相比之下,香山采用的预测方法是每次预测一个指令块。也就是说,给定一个 PC,香山会预测出以这个PC开始的一个分支预测块,包括了接下来若干条指令的情况,如是否存在分支指令、分支指令的位置、是否跳转以及跳转目标等信息

这样的预测方法可以一次性地预测出多条指令,并将预测结果送往取指单元(IFU),指导IFU进行取指。此外,由于IFU需要考虑缓存行的性能问题,它可以根据预测块一次性地取出多条指令,从而提高吞吐效率。

在预测块产生后,分支预测块还会生成执行完本预测块后跳转到的 PC,接着 BPU 会根据该 PC 继续产生下一个分支预测块

举个简单的例子

如上图所示,当 PC 执行到 0x20000118 时,BPU会经历如下几个步骤:

  1. BPU得知PC 0x20000118
  2. BPU产生以 0x20000118 为开始的分支预测块,内容大致如下
    1. 在接下来的若干条指令中
    2. 第三条是一个条件分支指令
    3. 对于这个条件分支指令,预测他将会跳转
    4. 跳转后的地址为 0x20000110
  3. BPU将PC设置为 0x20000110,并继续产生下一个分支预测块

这便是采用了分支预测块的香山 BPU 的基本预测流程

多预测器、多流水线结构

上图展示了香山BPU的总体架构,其中我们需要关注两个主要方面:

多预测器

  1. 为了确保预测的准确性,香山 BPU 采用了多个预测器,并且这些预测器共同产生 BPU 的预测结果。例如,FTB 会生成基础的预测结果供后续预测器使用,而 TAGE 则对条件分支指令产生更精准的预测结果等。

多流水线

  1. 为了满足高性能的需求,香山 BPU 采用了流水线设计。各个预测器处于不同的流水线级别。其中,uFTB(即图中的uBTB)预测器位于第一流水线级别,能够在一个周期内产生预测结果。而其余预测器则需要2-3个周期才能生成预测结果,尽管预测时间较长,但预测结果相对更精确。

但是,如果在第三个周期才能获取到预测结果,并基于新的结果重新开始预测,这样的设计难免导致性能损失。因为这样一来,需要耗费三个时钟周期才可以完成一次预测。

为了在第一和第二周期就能够获取到某些预测器的预测结果,我们设置了三个预测结果通道,并将三个流水线级别的预测结果同时输出,如下图所示。

取指目标队列(FTQ)

分支预测结果暂存

尽管 BPU 可以以分支预测块的形式提供预测结果,IFU 也可以一次性取指多条指令,但它们之间仍然存在性能上的差距。通常情况下,BPU产生预测结果的速度更快。

因此,在 BPU 与 IFU 之间添加了一个取指目标队列(FTQ)作为缓冲。FTQ 本质上是一个队列,用于存储一个个数据项。BPU产生的预测结果会先暂存到FTQ中,然后由 IFU 从 FTQ 中获取预测结果,如下图所示。

每当 BPU 产生一个预测块,该预测块会被放入 FTQ 的队首。当 IFU 处于空闲状态时,它会从 FTQ 队尾获取下一个预测块。下方的示意图展示了这一过程。

在香山中,FTQ 的功能远远不止于此。参考上文图中 FTQ 的对外交互接口,可以看出它还负责向 ICache 发送预取信息,存储 BPU 的各类训练信息,负责分析取指模块、后端执行模块中发来的重定向信息、更新信息,还会向 BPU 发送更新请求,甚至 FTB 预测器中的基本数据结构——FTB项都是由 FTQ 来更新的。

BPU 预测结果重定向

上文中提到,香山分支预测结果共有三个通道,会同时输出 s1, s2, s3 流水级的预测结果,那 FTQ 如何使用三个阶段的预测结果呢?

我们不妨从探讨流水线的时序出发,如下图所示

  • 在第一个周期,一个新的PC 0x4 被送入,能在一周期内产生预测结果的预测器(叫做 uFTB)在 s1 接口输出了他的预测结果,并且结果指示下一个 PC 为 0xf,其他接口还无输出
  • 在第二周期,PC 被设置为了 0xf,而 uFTB 也产生了 0xf 的预测结果,并从 s1 通道送出。与此同时,两周期预测器产生了上一轮 0x4 地址的预测结果,并从 s2 通道送出。

那么在这里产生了一个问题,第二周期 s2 产生的是 0x4 预测结果,而 0x4 的预测结果已经在上一周期被 s1 所输出,并放置在了 FTQ 中的某个项中。也就是说 s2 产生的预测结果,已经在 s1 通道被产生过了。但不同点是 s2 结果是由两周期预测器产生而来,结果会更加准确。

因此我们需要做的并不是根据 s2 的预测结果,放置一个新的 FTQ 项,而是 对比 s2 预测结果和上一周期 s1 预测结果是否有差异,如果有差异则覆盖上一阶段 s1 接口放置的 FTQ 项

于是我们给 s2 和 s3 通道增加两个额外的信号线,我们称之为 redirect 信号,如果该信号有效说明此时该阶段的预测结果与之前的预测结果产生差异,并且需要覆盖之前某个位置的 FTQ 项。结构如下图所示。

结构示意图中的时刻,对应流水线的第二个周期,此时 s1 通道已经往 FTQ 中放入一个地址为 0x4 的分支预测块结果,而此时 s2 预测结果产生,BPU 发现 s2 预测结果与 s1 不同,则本周期 redirect 接口被置为有效。FTQ 会使用 s2 通道的预测结果覆盖之前存放 0x4 预测结果的 FTQ 项。

此时,s1 通道虽然也产生了以 0xf 为首的分支预测块,但显然,0xf 是 s1 基于第一周期的错误预测结果产生的 PC,因此在这时候,s1 结果可直接弃之不用。

在第三周期,s1 又使用 s2 所产生的正确预测结果所指示的新PC 0x8 开始了新一轮预测。之后,如果 s2, s3 通道始终未检测到有预测错误发生,则流水线将以满状态持续运行下去。

BPU 重定向请求

无论再精确的分支预测器也不总是正确的,这种不正确会导致后续流水线中填充错误的指令,因此需要有一种机制来纠正这种错误,这种机制就是重定向。指令在被后端执行模块执行时,这条指令真正的行为才会被确定,因此在这个时候,后端执行模块如果发现分支预测错误,就会发出 重定向请求 (注意此处重定向请求与上文预测结果重定向是不同的概念),让处理器的状态恢复到执行错误指令之前的状态,对于我们来说,只需要关注BPU和FTQ在重定向时是怎样恢复状态的即可。

除了后端会产生重定向请求,香山处理器中会在 IFU 取到指令之后对指令进行简单分析,检测出最基本的预测错误。具体的过程是这样的,FTQ 向 IFU 发送取指请求之后,会等待 IFU 返回预译码信息(预译码是IFU对指令的简单解码,例如是否是跳转指令、跳转的目标是什么),FTQ 会将预译码信息写回到对应 FTQ项 中的某个字段,同时会分析预译码信息,如果发现预测错误,便会生成一个 IFU 重定向请求。

来自后端执行模块的重定向请求不需要 FTQ 生成,是直接由后端发往 FTQ 进行处理。FTQ 会将生成的 IFU 重定向请求与后端重定向请求转发到 BPU 的重定向接口中去,如果二者在同一周期有效,那么FTQ会选择转发后端重定向请求。

添加了重定向接口的 BPU 如下图所示。

BPU 的更新请求

目前的 BPU 已经有了纠错的能力,但是还有一个问题,BPU 中的数据没有办法得到更新。如果无法获取指令的位置、类型、是否发生跳转以及跳转地址等信息,BPU 将得不到训练,并导致准确度大幅降低。

为了获得这些信息,我们仍需依赖于取指目标队列(FTQ),因为它不仅能与 IFU 交互获取指令相关的信息,还可以与后端交互,获取执行的相关信息。 因此会有一个更新请求通道由 FTQ 直接连接至 BPU。

当后端执行完 FTQ 中的某一项时,该项会被标识为已提交,接下来 FTQ 将该项的更新信息,通过 Update 通道发往BPU。

总结

通过本节,我们了解到 BPU 对外交互所需的所有主要接口,以及 FTQ 对于 BPU 的作用。拥有了预测结果接口、重定向接口、更新接口的 BPU已经可以支撑 BPU 的所有对外交互了。接下来我们会将目光深入 BPU 内部。

1.3 - 香山分支预测单元结构介绍

本节介绍了香山分支预测单元(BPU)的结构,包括多预测器、多流水线方案的整合,以及内部子预测器的组织结构和接口设计,展示了BPU如何与Composer交互,并说明了子预测器之间的连接方式。

BPU 是如何整合内部子预测器的?

我们已经知道香山 BPU 采用的是多预测器,多流水线方案。其中为了适配多流水线,BPU 采用了三通道结果输出接口。但是又是如何适配多预测器的呢?这就要求我们进一步深入 BPU,探究其内部结构。

上图为香山文档中的 BPU 架构图,目前我们只需要关心这样一个信息,内部的所有子预测器都被包在了一个叫做 Composer 的结构中。BPU只需要和 Composer 交互。

Composer 是什么?我们不妨先看一下香山代码中对于他们的定义。

可以发现,Composer 以及五个子预测器有一个共同的特点,他们全部继承于 BasePredictor 基类。并且接口已经在 BasePredictor 类中定义好。换句话说就是,Composer和五个子预测器都拥有相同的接口!BPU 顶层可以直接把 Composer 也当做一个子预测器,而无需关心内部是怎么连接子预测器的。

子预测器接口

接下来我们会查看子预测器接口是怎样的。该接口将涉及到 Composer 与 BPU 顶层的交互,还会涉及到各子预测器与 Composer 的交互。

我们先以 Composer 为例,说明子预测器接口的结构

如上图所示,Composer 的三通道预测结果被直接输出至 BPU 外部,并且还有一组三通道预测结果从BPU内部连接至 Composer ,但由于预测结果是由 Composer 产生,因此 BPU 会将一个空的预测结果传递给 Composer ,这样做的意义是,使子预测器形成了一个“加工”的作用,子预测器会将输入的预测结果进行加工,然后再输出加工过后的预测结果。

接下来,BPU 顶层会为流水线提供预测所需要的信息。首先是 PC分支历史记录(包括全局历史和全局折叠历史),接下来 BPU 会和 Composer 之间连接一些流水线控制信号,最后 BPU 将外部输入的重定向请求接口更新接口直接连接至 Composer

最终可以简单给出子预测器接口的定义(详细定义请前往接口文档进行查看):

  • in
    • (s1, s2, s3) 预测信息输入
    • s0_pc 需要预测的PC
    • ghist 全局分支历史
    • folded_hist 全局折叠历史
  • out (s1, s2, s3) 预测信息输出
  • 流水线控制信号
    • s0_fire, s1_fire, s2_fire, s3_fire 相应流水级是否工作
    • s2_redirect, s3_redirect 后续流水级发现预测错误时重定向信号
    • s1_ready, s2_ready, s3_ready 子预测器相应流水级是否就绪
  • update 更新请求
  • redirect 重定向请求

子预测器之间的连接

我们已经清楚各个子预测器之间的接口与Composer 的接口是相同的,并且我们也已经知道了 Composer是如何连向顶层 BPU 的,本小节将会说明子预测器是如何在 Composer 内部进行连接的。

上图便是子预测器在 Composer 中连接的结构图。可以看出,三通道预测结果送入Composer后,首先经过 uFTB 的“加工”后输出,再依次由 TAGE-SCFTBITTAGERAS进行“加工”,最终连接到 Composer 的预测结果输出,并由Composer 再直接连往 BPU 外部。

而对于其他信号,因为Composer和各子预测器的接口相同,都被Composer直接连接到了每个预测器的相应接口,而没有做非常多额外处理。

预测结果接口连接

对于子预测器来说,他们的预测结果的连接方式是,预测结果输出是下一个预测器的输入。但需要注意的是,这种连接是组合电路的连接,而没有时序的影响。

如上图所示,以 s1 通道为例,从输入到最后一个预测器输出,中间全都是组合电路在做修改,不受时序影响。寄存器只在 s1, s2 和 s3 通道之间存在。

因此增加子预测器的数量,并不会导致预测所需的周期数量增多,仅仅会导致每个周期预测所需要的时延增大。

1.4 - 香山分支预测单元时序介绍

三级流水线的时序设计是香山 BPU 的灵魂所在。预测结果重定向信号如何产生,如何根据预测结果生成新的PC,如何处理三个通道的预测结果,都会在本节中进行介绍。

一周期无空泡预测

uFTB 是香山 BPU 中唯一一个可以在一周期便可以产生预测结果的预测器,上图中展示了 uFTB 的预测过程。s0_pc 由 BPU 顶层送入,s1 周期生效时,s1_pc 保存了上一周期 s0_pc 的值,依次类推。也就是说传入的 s0_pc 的值会随流水线向下移动。

在 s1 周期生效时,uFTB 会接收到本周期传来的 s1_fire 信号,并根据 s1_pc 指示的地址,在本周期生成预测结果,在预测结果中可以获取新的 PC 值。

如图所示,BPU 顶层根据 s1 预测结果通道,分析出下一个 PC 值的位置(即图中的 target),并将其送往 npc_Gen (即新PC生成器)中,用于产生下一个周期的 s0_pc。

于是下一周期,uFTB 获取到了新的 PC 值,并开始了新 PC 值预测块的产生。由此可见,仅凭借 s1 周期,便可以以一周期一个预测块的速度来产生预测结果。

预测结果重定向

但除了 uFTB 以外,其他预测器都需要 2-3 个周期才可以产生预测结果,如何利用起他们的预测结果?又如何生成预测结果重定向信号呢?

如图中所示,一个两周期产生预测结果的 Predirector 2 ,可以在 s2 周期,向 s2 预测结果通道内输出它的预测结果。BPU 顶层拿到预测结果后,分析出预测块的跳转目标地址 target 并连向 npc_Gen

此时连向 npc_Gen 的信号中,既有 s2 产生的旧 PC 的预测结果,又有 s1 产生的新 PC 的预测结果,该如何抉择新 PC 用哪一个呢?

通过之前的介绍,我们已经知道,BPU 会将 s2 的预测结果与上周期 s1 的预测结果进行比较,如果预测结果不同,那么说明 s1 预测错了,自然 s1 基于上周期错误预测结果产生的本周期预测结果也是错误的,因此在本周期如果预测结果产生错误,npc_Gen 将使用 s2 所提供的 target 作为新的 s0_pc

这个过程画在流水线结构图中是这样的:

Diff 比较器通过获取 s1 周期的预测结果,与 s1 周期的预测结果进行对比产生 diff 信号,指导 npc_Gen 进行下一条 pc 的生成。与此同时,diff 信号指示了 s1 阶段的预测结果发生错误,可直接用于 BPU 发往 FTQ 的预测结果中 s2 通道的预测结果重定向通道,指导 FTQ 覆盖之前的预测结果。

Diff 信号还会通过 s2_redirect 接口送往每个子预测器,指导子预测器进行状态的更新。

除此之外,当 s2 预测结果重定向发生时,说明 s1 通道预测结果已经发生错误,s2 阶段不能继续进行预测,需及时将子预测器流水线控制信号 s2_fire 置为无效,并等待纠正后的预测结果流入。

s3 预测结果的重定向与此类似,其流水线结构图如下所示。具体的处理过程留给大家来分析。

重定向请求与其他信息生成

只有当三个阶段的预测信息都错误时,才会产生有外部重定向请求发生,此时 npc_Gen 会接收到来自重定向请求中的 pc 地址。由于当重定向请求发生时,我们认为三阶段均预测错误,因此需要将三个阶段的 fire 信号全部置为无效,然后 npc_Gen 采用重定向请求中需要恢复的 PC,重新开始预测。

其他信息,例如全局历史的生成方式与PC的生成方式一致,都是在顶层通过三阶段预测信息来维护,全局历史会根据每一阶段的预测结果来产生新的分支历史。

流水线控制信号

学习了流水线的具体流程,我们应该可以理解子预测器接口中的流水线控制信号了,如下

  • s0_fire, s1_fire, s2_fire, s3_fire 指示每个流水级是否工作
  • s2_redirect, s3_redirect 指示是否有预测结果重定向发生
  • s1_ready, s2_ready, s3_ready 子预测器发给BPU顶层,表示相应流水级是否就绪

总结

至此,你应该懂得了香山分支预测单元的基础设计思想、对外交互逻辑、内部结构、时序等内容,并对 BPU 的工作原理有了大致的理解,香山的 BPU 对你来说已经不再神秘。

接下来你可以阅读重要结构及接口文档,并结合香山 BPU 源代码对 BPU 形成更为细致的理解,当你能清楚的明白 BPU 的工作原理和信号细节时,便可以开始你的验证工作了!

2 - 重要结构及接口文档

本文档将会对 BPU 中的重要结构以及对外接口进行描述,描述粒度将深入代码级别,通过阅读本文档,你可以理解香山分支预测单元中每一个信号是什么作用,理解各种请求的具体实现方式,也将使你能够结合代码进行功能的理解。

本文档将会对 BPU 中的重要结构以及对外接口进行描述,描述粒度将深入代码级别,文档中描述的结构将与香山分支预测单元 Chisel 版本代码保持一致,信号结构及名称亦来自于 Chisel 版本代码。

文档适用于已经了解了香山分支预测单元基础设计,并想要深入了解信号细节的同学使用。可以根据验证需要的内容有选择的进行阅读,或随时进行查阅。

其中,FTB项与完整预测结果接口涉及 BPU 产生预测结果的方式,建议每位同学进行阅读。

2.1 - FTB项与完整预测结果接口

BPU 的基本预测单位是分支预测块,然而分支预测块的结构是怎样的呢?为什么一个分支预测块可以指导接下来如此多条指令的执行呢?这一部分将讲述生成分支预测块的核心数据结构——FTB项,以及由 FTB 项生成的完整预测结果结构。

FTB 项

FTB 项是香山中分支预测块的核心数据结构,它存储了生成一个分支预测块所需的信息。在BPU进行预测时,初始的分支预测块首先由一个读出的 FTB 项生成。然后,这个分支预测块会传递到后续的预测器中,后续的预测器会读取其中的信息并加以修改,生成最终的预测结果。

因此,要理解分支预测块的结构,首先需要理解FTB项的结构。一个FTB项对应着一个分支预测块,其大致结构如下:

首先,需要明确一个信息:无论是分支预测块还是FTB项,它们可以容纳的指令数量都被设定为一个特定的上限(在香山当前版本中为16条RVC指令),被称为最大预测长度。这意味着,如果我们需要记录分支预测块内某一条指令的位置,可以使用一个固定长度的位向量,来指定这条指令相对于预测块起始地址的偏移量。

分支预测块执行流程的决定因素是其中的分支指令信息。其余的指令都被视为普通指令,不影响程序的执行流。因此,在一个分支预测块中,我们只需记录下其中的分支指令位置即可,而普通指令的位置我们并不关心。

因此,FTB项中定义了两种类型的分支指令槽(slot)——brSlotstailSlot,用于存储分支预测块中的分支指令。目前香山版本中,brSlots只含有一个 slot,而 tailSlot 则是一个单独的 slot,总计含有两个 slot。

在最大预测长度内的指令中,如果出现了分支指令,FTB项会将其记录在对应的slot内,并将该slot置为有效。如果分支指令出现过多,达到了FTB项的容纳上限,那么超出上限的分支指令则会交给下一个FTB项来存储。在每个 slot中,我们会记录一条分支指令相对于预测块起始地址的偏移量(offset),同时还会记录其跳转目标地址等信息。

唯一的 tailSlot

在RISC-V中,分支指令主要分为两种类型:条件分支和无条件跳转。因此,对于一个分支预测块来说,最多只会包含一条无条件跳转指令。因为一旦执行到这条指令,程序的执行流就会发生改变,后续的指令将不再执行。因此,我们定义了一种类型的 slot 叫做 tailSlot,专门用来存储这条无条件跳转指令。而对于条件分支指令,则将它们存储在brSlots中。

tailSlot如其名,位于整个预测块中的最后一个 slot。这也是因为一旦填充了无条件跳转指令,程序必定跳转,之后的指令就由其他预测块进行处理,因此后续的指令情况我们无需关心。但在无条件跳转指令之前的指令中,我们需要关心是否存在条件分支指令,因为条件分支指令既有可能跳转,也有可能不跳转。因此,我们需要记录下条件分支指令的相关信息。

tailSlot 共享

考虑一种情况:如果从预测块起始PC开始直到最大预测长度,都没有出现无条件跳转指令,反而出现了两条条件分支指令,这样一来,tailSlot 就会空闲,而第二条条件分支指令又无法存储,造成空间的浪费。

为了解决这个问题,香山采用了一种方法:设置一个 sharing 标记。我们可以直接将第二条分支指令存储到tailSlot 中,并将sharing标记设置为真,表示第二条条件分支指令共享了无条件跳转指令的 tailSlot。这样,就有效利用了tailSlot的空间。

预测块中的 isCallisRetisJalr字段是为tailSlot服务的。如果tailSlot中记录的是无条件跳转指令,这几个字段会进一步指示该跳转指令的类型。在FTB项中还有一个字段 always_taken,用来记录每个Slot中存储的条件分支指令是否总是被预测为跳转。如果是的话,后续的预测器可以直接采用这一预测结果。

通过FTB项,我们可以得知一个分支预测块中的指令情况,包括分支指令的位置、类型等。这些信息会交给后续的预测器,由后续的预测器来预测更为精确的跳转目标、是否跳转等信息。

FTB 项完整结构 (FTBEntry)

接口定义: src/main/scala/xiangshan/frontend/FTB.scala

本节中描述了 FTB 项的完整结构定义,其所含的信号如下:

  • valid: FTB表项是否有效。
    • 接口类型: Bool
  • brSlots: 条件分支指令槽。
    • 接口类型: Vec(numBrSlot, new FtbSlot(BR_OFFSET_LEN)) (接口列表详见FtbSlot)
  • tailSlot: 无条件跳转指令槽。
    • 接口类型: new FtbSlot(JMP_OFFSET_LEN, Some(BR_OFFSET_LEN)) (接口列表详见FtbSlot)
  • pftAddr: 预测块的结束地址。
    • 接口类型: UInt(log2Up(PredictWidth).W)
  • carry: 预测块的结束地址进位。
    • 接口类型: Bool()
  • isCall: 无条件跳转指令槽中的指令为 call 指令。
    • 接口类型: Bool()
  • isRet: 无条件跳转指令槽中的指令为 ret 指令。
    • 接口类型: Bool()
  • isJalr: 无条件跳转指令槽中的指令为 jalr 指令。
    • 接口类型: Bool()
  • last_may_be_rvi_call: 无条件跳转指令槽中的指令可能为一个 RVI 类型的 call 指令信号。
    • 接口类型: Bool()
  • always_taken: 该预测块中的每个分支指令是否总是被预测为 Taken。
    • 接口类型: Vec(numBr, Bool())

说明:pftAddr 和 carry

在这里,pftAddr是Partial Fallthrough Addr的缩写。Fallthrough Addr 表示,如果预测块中没有跳转,那么程序将会顺序执行到达的地址。换句话说,如果一条无条件跳转指令的偏移为5,那么 Fallthrough Addr 对应的偏移便是6。这一信号主要用于获取函数调用后程序的返回地址,可以将这一概念理解为预测块的结束地址。

Partial 表示部分地址,这是由地址的表示方法决定的。在这里,地址的表示方法如下:

  pc: | ... |<-- log(predictWidth) -->|<-- log(instBytes) -->|
           ^                         ^
           |                         |
           carryPos                  instOffsetBits

pftAddr 便是只记录了中间的偏移部分(即长度为 log(predictWidth) 的部分),之后结合当前 PC 才能生成完整 PC。但由于有可能产生进位,因此单独记录一个 carry 位,carryPos 是预测块中的指令地址中 会产生进位的位置

另外,last_may_be_rvi_call 也是对该地址的一个辅助信号,表示无条件跳转指令槽中的指令是一个RVI类型的 call 指令。由于pftAddr在计算时默认认为指令长度为压缩指令长度,因此计算结束地址只会加 2 字节。但如果实际的call指令并不是压缩指令,就会导致返回地址计算错误。RAS会根据这一信号修正这个错误。

分支预测槽 (FTBSlot)

接口定义: src/main/scala/xiangshan/frontend/FTB.scala

该接口定义了 FTB 表项中 slot:

  • offset: 该 slot 中指令相对于预测块起始地址的偏移
    • 接口类型:UInt(log2Ceil(PredictWidth).W)
  • lower: 跳转目标地址的低位。
    • 接口类型:UInt(offsetLen.W)
    • 说明:这里设置 lower 为 12 位或 20 位,因为分支指令的寻址能力为 12 位,而跳转指令的寻址能力为 20 位
  • tarStat: 跳转后的 pc 高位是否进退位
    • 接口类型:UInt(TAR_STAT_SZ.W)(TAR_STAT_SZ = 2)
    • 说明:跳转目标地址由当前 PC 的高位、tarState 以及 lower 字段共同计算得出,其中,lower 字段直接存储了跳转目标地址的低位。当前的 PC 高位经过 tarStat 指导进行进退位之后,直接与 lower 拼接便可得到真正的跳转目标地址。tarStat 共有三个可能的取值:0 - 不进不退, 1 - 进位, 2 - 退位。
  • sharing: 无条件跳转指令槽中存储了一条条件分支指令。
    • 接口类型:Bool()
  • valid: slot 有效位。
    • 接口类型:Bool()

完整分支预测结果 (FullBranchPrediction)

  • 接口定义: src/main/scala/xiangshan/frontend/FrontendBundle.scala

该接口定义了完整的分支预测结果,每流水级预测结果中都会包含该接口。

完整分支预测结果接口与 FTB 项相差无几,并最初由 FTB 项生成。两个Slot被拆分成了单独的信号:slot_valids, targets, offsets, is_br_sharing 等,另外增加了若干字段例如 br_taken_mask, jar_target 便于后续预测器提供精确预测结果,另外 hit 表示 FTB 项是否命中,即本轮预测中的 PC 索引到了某个 FTB 项。

完整接口列表如下:

  • hit FTB表项是否命中

    • 接口类型:Bool()
  • slot_valids slot有效位,表示 ftb 项中的每个 slot 是否有效

    • 接口类型:Vec(totalSlot, Bool())
  • targets 每个 slot 对应的跳转目标地址

    • 接口类型:Vec(totalSlot, UInt(VAddrBits.W))
  • offsets 每个 slot 中的指令相对于预测块起始地址的偏移

    • 接口类型:Vec(totalSlot, UInt(log2Ceil(PredictWidth).W))
  • fallThroughAddr 预测块的结束地址

    • 接口类型:UInt(VAddrBits.W)
  • fallThroughErr FTB项中记录的 pftAddr 有误

    • 接口类型:Bool()
  • is_jal 预测块内包含jal指令

    • 接口类型:Bool()
  • is_jalr 预测块内包含jalr指令

    • 接口类型:Bool()
  • is_call 预测块内包含call指令

    • 接口类型:Bool()
  • is_ret 预测块内包含ret指令

    • 接口类型:Bool()
  • last_may_be_rvi_call 无条件跳转指令槽中的指令可能为一个 RVI 类型的 call 指令信号。

    • 接口类型:Bool()
  • is_br_sharing 最后一个slot (tailSlot) 中存储的是条件分支指令信号

    • 接口类型:Bool()
  • br_taken_mask 分支预测结果,每个分支(slot)对应一位,表示该分支是否被预测为 taken

    • 接口类型:Vec(numBr, Bool())
  • jalr_target 本预测块中的 jalr 的跳转目标

    • 接口类型:UInt(VAddrBits.W)

2.2 - 重定向与更新接口

重定向与更新请求是 FTQ 发往 BPU 的两大信息类型,本节中讲述了这两种请求接口的具体结构。

分支预测重定向(BranchPredictionRedirect)

接口定义:src/main/scala/xiangshan/frontend/FrontendBundle.scala

接口类型:BranchPredictionRedirect

该接口定义了分支预测单元的重定向请求,主要用于重定向分支预测器的状态。

BranchPredictionRedirct 接口继承自 Redirct 接口,所含信号较多,BPU 重定向只会用到其中一部分接口,文档描述的结构中只包含 BPU 用到的接口内容。

重定向请求有两种来源,与 IFU 预译码信息对比生成的重定向请求,后端执行过程中的重定向请求。

在重定向请求中,关键信息是cfiUpdate即控制流指令信息。这一信息对应着一条指令,也就是BPU发生预测错误的指令。举例来说,如果 BPU 在预测块中指示第三条指令为普通指令不发生跳转,但预译码显示第三条指令是一条无条件跳转指令,必定会跳转。在这种情况下,BPU 发生了预测错误,FTQ 产生了重定向请求。在重定向请求中,cfiUpdate 将会对应这条无条件跳转指令。

cfiUpdate 中的信息可大致分为三种类型的信息:

  1. 对应指令的信息及执行情况。其中会说明该指令在预测块中位于那个槽 (shift) 及 pc; 会指示这条指令的类型相关信息 (pd);还有这条指令的执行情况,例如跳转目标、是否跳转等
  2. 历史维护相关信息。重定向请求中包含了该条指令所处的预测块对应的分支历史信息,以帮助BPU恢复分支历史信息。foleded_hist 代表全局折叠历史,histPtr 代表全局历史指针,而其余信息则是维护分支历史的辅助信息。要详细了解这些信息的含义,请参阅 BPU顶层模块
  3. RAS维护相关信息。详细含义请参阅 RAS 子预测器文档。

level 信息的含义是,重定向是否包含本条指令,如果不包含,重定向请求的接收方将认为这条指令已经被执行了,下次预测将从下一条指令开始。BPU 顶层会默认不包含本条指令,收到重定向请求后,会将本条指令的执行情况统计到分支历史当中。

以下为重定向接口的详细信号列表:

  • level: 重定向请求是否包括本位置,低表示在本位置后重定向,高表示在本位置重定向。

    • 接口类型: UInt(1.W)
  • cfiUpdate: 控制流指令更新信息

    • 接口类型: CfiUpdateInfo

    • 接口列表

      • pc: 重定向请求对应的指令 PC
        • 接口类型: UInt(VaddrBits.W)
      • pd: 重定向指令的预译码信息
        • isRVC: 是否是压缩指令
          • 接口类型: Bool
        • isCall: 是否是函数调用
          • 接口类型: Bool
        • isRet: 是否是函数返回
          • 接口类型: Bool
      • target: 重定向请求指令的目标地址。
        • 接口类型: UInt(VaddrBits.W)
      • taken: 重定向请求指令是否发生跳转。
        • 接口类型: Bool
      • shift: 重定向请求指令位于哪个槽,如果是普通指令则为0.
        • 接口类型: UInt((log2Ceil(numBr)+1).W)
      • addIntoHist: 重定向请求指令执行信息是否要加入分支历史。
        • 接口类型: Bool

      • folded_hist: 重定向请求对应的折叠历史。
        • 接口类型: AllFoldedHistories(foldedGHistInfos)
      • afhob: 重定向请求指令对应的分支历史最老位。
        • 接口类型: AllAheadFoldedHistoryOldestBits(foldedGHistInfos)
      • lastBrNumOH: 重定向请求对应的上次跳转位置。
        • 接口类型: UInt((numBr+1).W)
      • histPtr: 重定向请求需要恢复的全局历史指针。
        • 接口类型: CGHPtr

      • ssp: 重定向请求指令对应的 RAS 推测栈栈顶在提交栈位置的指针。
        • 接口类型: UInt(log2Up(RasSize).W)
      • sctr: 重定向请求指令对应的 RAS 推测栈栈顶递归计数 Counter。
        • 接口类型: UInt(log2Up(RasCtrSize).W)
      • TOSW: 重定向请求指令对应的 RAS 推测栈(队列)写指针。
        • 接口类型: CGHPtr
      • TOSR: 重定向请求指令对应的 RAS 推测栈(队列)读指针。
        • 接口类型: CGHPtr
      • NOS: 重定向请求指令的 RAS 栈顶 Counter。
        • 接口类型: CGHPtr

分支预测更新(BranchPredictionUpdate)

接口定义: src/main/scala/xiangshan/frontend/FrontendBundle.scala

该接口定义了分支预测器的更新请求,主要用于更新分支预测器的状态,文档只列出了在 BPU 中使用的接口。

更新请求与每一个分支预测块一一对应,当 FTQ 中的一个分支预测块被执行过以后,FTQ 将会为这个分支预测块产生一个更新请求,来指导预测器进行训练,因此更新请求中的一个重要职责就是向 BPU 反馈指令的真实执行情况。当然在香山分支预测单元中,更新请求还会负责 FTB 项的更新

更新请求中的信息可大致分为四类:

  • PC 表示预测块的起始地址,指示了该更新请求对应哪个分支预测块
  • FTB 项更新信息 更新通道中会含有一个 FTB 项结构 (ftb_entry),输出 FTQ 新生成的 FTB 项,并且还会指示是否与旧的 FTB 项相同 (old_entry)
  • 指令真实执行情况信息 更新通道中会指示该分支预测块中分支指令和无条件跳转指令的执行情况,还会给出控制流指令(即发生跳转的指令)的地址以及最终跳转目标。
  • 与该预测块对应的预测器相关数据 包含 spec_info 以及 meta 信息。(具体请参阅BPU全局接口文档)

更新请求的接口列表如下:

  • pc 更新请求的pc (预测块起始地址)
    • 接口类型:UInt(VAddrBits.W)

  • ftb_entry 更新后的ftb项
    • 接口类型:new FTBEntry()
    • 接口列表:详见(FTBEntry
  • old_entry 更新后的 FTB 项是否与旧的 FTB 项相同
    • 接口类型:Bool()

  • br_taken_mask 预测块内每个slot内指令是否跳转
    • 接口类型:Vec(numBr, Bool())
  • mispred_mask 预测块内预测错误的掩码。第一、二位分别代表两个条件分支指令是否预测错误,第三位指示无条件跳转指令是否预测错误。
    • 接口类型:Vec(numBr+1, Bool())
  • jmp_taken 预测块内无条件跳转指令被触发
    • 接口类型:Bool()
  • cfi_idx 控制流指令在预测块中的索引
    • 接口类型:ValidUndirectioned(UInt(log2Ceil(PredictWidth).W))
  • full_target 预测块的跳转目标(下一个预测块的起始地址)
    • 接口类型:UInt(VAddrBits.W)

  • spec_info 该预测块对应的 last_stage_spec_info
    • 接口类型:new SpeculativeInfo
    • 接口列表:(只用到了 foled_hist 一项)
      • folded_hist 全局折叠历史
        • 接口类型:AllFolededHistories(foldedGHistInfos)
  • meta 该预测块对应的 last_stage_meta 信息
    • 接口类型:UInt(MaxMetaLength.W)

2.3 - BPU 全局接口

本节中介绍了香山分支预测单元整体对外交互的接口的定义,包括全局分支预测结果和单流水级预测结果的呈现形式。

BPU 模块整体对外接口 (PredirectIO)

接口定义: src/main/scala/xiangshan/frontend/BPU.scala

PredirectIO 是分支预测器整体的对外接口,它主要处理了分支预测器(BPU)与取指目标队列(FTQ)之间的交互,主要包含以下几个部分:

  • bpu_to_ftq: BPU 向 FTQ 发送的信息,主要用于 BPU 向 FTQ 发送分支预测结果
    • 接口类型: BpuToFtqIO
    • 信号列表:
      • resp: BPU 向 FTQ 发送的全局分支预测信息
        • 接口类型:DecoupledIO(new BpuToFtqBundle())
          • BpuToFtqBundle 继承自 BranchPredictionResp,没有额外的信号
        • 信号列表:详见 (BranchPredictionResp)
  • ftq_to_bpu: FTQ 向 BPU 发送的信息,主要用于处理重定向及更新请求
    • 接口类型: FtqToBpuIO
    • 信号列表:
      • redirect: FTQ 向 BPU 发送的重定向请求
        • 接口类型: Valid(new BranchPredictionRedirect)
        • 接口列表:详见(BranchPredictionRedirect
      • update: FTQ 向 BPU 发送的更新请求
        • 接口类型:Valid(new BranchPredictionUpdate)
        • 接口列表:详见(BranchPredictionUpdate
      • enq_ptr: FTQ发送给BPU的FTQ指针,告诉BPU下一次需要写到哪个FTQ表项
        • 接口类型:FtqPtr
  • ctrl: BPU 控制信号,主要用于控制各预测器的使能
    • 接口类型:BPUCtrl
    • 接口列表:
      • ubtb_enable: UBTB 预测器使能
        • 接口类型:Bool()
      • btb_enable: BTB 预测器使能
        • 接口类型:Bool()
      • bim_enable: BIM 预测器使能
        • 接口类型:Bool()
      • tage_enable: TAGE 预测器使能
        • 接口类型:Bool()
      • sc_enable: SC 预测器使能
        • 接口类型:Bool()
      • ras_enable: RAS 预测器使能
        • 接口类型:Bool()
      • loop_enable: LOOP 预测器使能
        • 接口类型:Bool()
  • reset_vector: 重置向量,reset 时 BPU 的 pc 会被重置为该值
    • 接口类型:UInt(PAddrBits.W)

全局分支预测信息 (BranchPredictionResp)

接口定义: src/main/scala/xiangshan/frontend/FrontendBundle.scala

该接口定义了分支预测器预测的所有结果信息,包含了每一阶段的预测结果,以及最后一个流水级输出的相关信息。

  • s1 s1流水级的分支预测结果
  • s2 s2流水级的分支预测结果
  • s3 s3流水级的分支预测结果
    • 接口类型:BranchPredictionBundle
    • 信号列表:详见(BranchPredictionBundle
  • last_stage_meta 最后一个流水级的输出的本轮预测的预测器元数据。是一个位向量,由每个预测器输出、并由 Composer 合并为一个。
    • 接口类型:UInt(MaxMetaLength.W)
  • last_stage_spec_info 最后一个流水级的输出的本轮预测的相关信息
    • 接口类型:Ftq_Redirect_SRAMEntry
    • 接口列表:
      • folded_hist 全局折叠历史
        • 接口类型:AllFoldedHistories(foldedGHistInfos)
      • afhob 全局分支历史最老位
        • 接口类型:AllAheadFoldedHistoryOldestBits(foldedGHistInfos)
      • lastBrNumOH 上次跳转位置。
        • 接口类型:UInt((numBr+1).W)
      • histPtr 全局分支历史指针
        • 接口类型:CGHPtr
      • ssp RAS推测栈栈顶在提交栈位置的指针
        • 接口类型:UInt(log2Up(RasSize).W)
      • sctr RAS推测栈栈顶递归计数 Counter
        • 接口类型:UInt(log2Up(RasCtrSize).W)
      • TOSW RAS推测栈(队列)写指针
        • 接口类型:CGHPtr
      • TOSR RAS推测栈(队列)读指针
        • 接口类型:CGHPtr
      • NOS RAS栈顶 Counter
        • 接口类型:CGHPtr
      • topAddr RAS栈顶返回地址
        • 接口类型:UInt(VAddrBits.W)
  • last_stage_ftb_entry 最后一个流水级的输出的FTB表项
    • 接口类型:FtqEntry
    • 接口列表:详见(FtqEntry

单流水级分支预测结果 (BranchPredictionBundle)

接口定义: src/main/scala/xiangshan/frontend/FrontendBundle.scala

该接口定义了每个流水级对外输出的分支预测结果信息,

  • pc 预测块的起始 pc
    • 接口类型:Vec(numDup, UInt(VAddrBits.W)) numDup仅为寄存器复制,其中信号完全相同
  • valid 预测结果是否有效
    • 接口类型:Vec(numDup, Bool())
  • hasRedirect 是否需要重定向
    • 接口说明:只有s2, s3阶段会重定向,重定向发生时本阶段预测结果会覆盖之前流水级的结果
    • 接口类型:Vec(numDup, Bool())
  • ftq_idx FTQ指针,指向该阶段对应预测信息的FTQ表项
    • 接口类型:new FtqPtr
  • full_pred 完整的分支预测结果
    • 接口类型:Vec(numDup, new FullBranchPrediction)
    • 接口列表:详见(FullBranchPrediction

2.4 - 子预测器基类与子预测器接口

本文档介绍了香山BPU中的子预测器接口,以及子预测器基类的使用,阅读此文档可帮助你了解子预测器的对外交互,和子预测器基类中信号的使用。

在香山分支预测单元中,其所有子预测器以及 Composer 的类实现都是继承自子预测器基类(BasePredictor),并且子预测器接口(BasePredictorIO)也是在子预测器基类中进行定义。因此我们可以认为,Compser 和所有子预测器都含有相同的接口。

在子预测的理解及验证当中,我们最直接的外界交互是发生在子预测器接口,以及子预测器基类中定义的一些变量,因此在子预测器的验证之前,强烈建议你阅读并理解本节文档。

子分支预测器接口的大致内容及使用方法,已在香山分支预测单元(BPU)基础设计中进行了介绍,本节文档将专注于接口的信号细节。

子分支预测器接口 (BasePredictorIO)

接口定义: src/main/scala/xiangshan/frontend/BPU.scala

每个子分支预测器都需要实现该接口,该接口定义了子分支预测器的输入输出接口。

注意:其中某些信号被定义为了 numDup 个数量,其中每个信号完全一样,多个相同信号是为其他因素考虑。

详细的信号列表如下:

  • reset_vector 重置向量,reset 时 BPU 的 pc 会被重置为该值

    • 接口类型:UInt(PAddrBits.W)
  • in BPU 向子分支预测器发送的信息

    • 接口类型:DecoupledIO(new BasePredictorInput)
    • 信号列表:
      • s0_pc s0流水级的pc
        • 接口类型:Vec(numDup, UInt(VAddrBits.W))
      • folded_hist 全局折叠历史信息
        • 接口类型:Vec(numDup, new AllFoldedHistories(foldedGHistInfos))
        • 信号列表:详见(AllFoldedHistories
      • ghist 全局分支历史信息
        • 接口类型:UInt(HistoryLength.W)
      • resp_in 全局分支预测信息(包含s1, s2, s3 预测结果信息)
        • 接口类型:BranchPredictionResp
        • 信号列表:详见(BranchPredictionResp
  • out 子分支预测器向 BPU 发送的信息(包含s1, s2, s3 预测结果信息)

    • 接口类型:new BasePredictorOutput 继承自 BranchPredictionResp
    • 信号列表:详见(BranchPredictionResp
  • ctrl BPU 子预测器使能控制信号,主要用于控制各预测器是否开启

    • 接口类型:BPUCtrl
    • 接口列表:
      • ubtb_enable: UBTB 预测器使能
        • 接口类型:Bool()
      • btb_enable: BTB 预测器使能
        • 接口类型:Bool()
      • bim_enable: BIM 预测器使能
        • 接口类型:Bool()
      • tage_enable: TAGE 预测器使能
        • 接口类型:Bool()
      • sc_enable: SC 预测器使能
        • 接口类型:Bool()
      • ras_enable: RAS 预测器使能
        • 接口类型:Bool()
      • loop_enable: LOOP 预测器使能
        • 接口类型:Bool()
  • s0_fire s0阶段握手成功信号

    • 接口类型:Vec(numDup, Bool())
  • s1_fire s1阶段握手成功信号

    • 接口类型:Vec(numDup, Bool())
  • s2_fire s2阶段握手成功信号

    • 接口类型:Vec(numDup, Bool())
  • s3_fire s3阶段握手成功信号

    • 接口类型:Vec(numDup, Bool())
  • s2_redirect s2阶段重定向信号

    • 接口类型:Vec(numDup, Bool())
  • s3_redirect s3阶段重定向信号

    • 接口类型:Vec(numDup, Bool())
  • s1_ready s1阶段是否准备好接收信息 (方向:由子预测器输出)

    • 接口类型:Bool()
  • s2_ready s2阶段是否准备好接收信息 (方向:由子预测器输出)

    • 接口类型:Bool()
  • s3_ready s3阶段是否准备好接收信息 (方向:由子预测器输出)

    • 接口类型:Bool()
  • update BPU 向子分支预测器转发的更新请求

    • 接口类型:Valid(new BranchPredictionUpdate)
    • 信号列表:详见(BranchPredictionUpdate
  • redirect BPU 向子分支预测器转发的重定向请求

    • 接口类型:Valid(new BranchPredictionRedirect)
    • 信号列表:详见(BranchPredictionRedirect

其中,流水线控制信号将会在下面内容中进行进一步的说明。

全局折叠历史 (AllFoldedHistories)

接口定义:src/main/scala/xiangshan/frontend/FrontendBundle.scala

接口类型:AllFoldedHistories(foldedGHistInfos))

全局折叠历史的接口信息仅为一个 FoldedHistory 的列表

  • hist 折叠历史列表
    • 接口类型:MixedVec(gen.map{case (l, cl) => new FoldedHistory(l, cl, numBr)})

FoldedHistory 的接口信息也仅有一项

  • folded_hist 单项折叠历史,其位宽为历史压缩后长度。
    • 接口类型:UInt(compLen.W)

也就是说,全局折叠历史接口实际上是一个存储了折叠后历史的列表,其中每一项都是一个特定长度的折叠历史。

子预测器基类

在子预测器基类中定义了若干信号,并在每个子预测器中都可以访问到,并在其中还进行了若干连线。

其中多数信号都较容易理解 ,我们需要重点关注的是其中每个流水的 pc,这还会涉及到你对于流水线控制信号的掌握。因此接下来,我们将会介绍在子预测器中需要关注的流水线控制信号的含义,以及 s1_pc, s2_pc, s3_pc 信号的含义。

其中流水线控制信号共分为 3 组:

  • fire 信号 (s0, s1, s2, s3)
  • redirect 信号 (s2, s3)
  • ready 信号 (s1, s2, s3)

子预测器基类中的 pc 信号共有四组,s0_pc_dup, s1_pc_dup, s2_pc_dup, s3_pc_dup。每组信号中有多个 pc 信号,他们完全相同,为考虑一些其他因素所复制。因此我们可单纯把他们看做 s0_pc, s1_pc, s2_pc, s3_pc

他们的用法可参照下图:

他们与流水线控制信号的关系是:

  • s0_pc 直接由输入接口中的 in.s0_pc 连接而来
  • s0_fire 生效时,下一周期 s1_pc 会输出 s0_pc 的值
  • s1_fire 生效时,下一周期 s2_pc 会输出 s1_pc 的值
  • s2_fire 生效时,下一周期 s3_pc 会输出 s2_pc 的值

也就是说,fire 信号是会影响到下一个周期的数据是否有效,比如 s0_fire 信号会影响 s1 流水的数据是否有效,s1_fire 信号会影响到 s2 流水的数据是否有效。

fire 信号是否有效取决于本流水级数据是否有效 以及 下一流水级是否 ready。例如 s1_fire 信号有效的条件必须包含 s1 阶段数据有效,以及子预测器输出的 s2_ready 信号为有效,此时便可认为,s1 阶段数据处理完成,并且 s2 阶段就绪,下一周期数据将被直接送入 s2 阶段。

因此在子预测器中,以 s1 阶段为例,s1_ready 可以阻塞数据进入 s1 阶段,当 s1_ready 生效时,下一周期 s1 阶段的数据便会生效。而 s1_fire 有效的同时,说明 s1 阶段数据已经生效,预测器也已经将 s1 阶段的结果生成,下一周期将直接被送入 s2 阶段。

redirect 信号则相对明确。以 s2 为例,当 s2_redirect 有效时,说明 s2_fire 生效的同时 s2 预测结果与上周期 s1 预测结果不同。

3 - 子模块文档

本部分文档将会详细介绍香山分支预测单元的每一个模块,包括 BPU 顶层和五个子预测器。

本部分文档将会详细介绍香山分支预测单元的每一个模块,包括 BPU 顶层和五个子预测器。

在每一个模块的文档中,我们将会详细介绍该模块在香山分支预测单元中所发挥的作用,以及该模块的算法原理、模块结构和时序等。

负责一个模块验证的同学需要重点阅读对应文档并结合代码进行理解,其他文档亦可进行阅读以帮助你对香山分支预测单元整体功能的理解。理解过程中,你可能需要时刻回顾之前文档中所描述的基础设计思想及接口信号的含义。

3.1 - BPU 顶层模块

BPU 顶层整体的功能和结构已经在之前的文档中粗略的描述,对于验证 BPU 顶层的同学来说,可能还需要更加细致的描述。由于 BPU 顶层功能较多,本节将 BPU 划分为了几大功能点进行更进一步的描述。但由于 BPU 顶层细节过多,更进一步的细节需参照代码进行理解。

生成器维护方法

通过香山的基础设计文档,我们知道,BPU 顶层是通过一个个生成器来维护 s0 周期的各类变量的,例如 PC、分支历史等,并且其核心概念是,通过预测结果重定向信号决定采用哪个流水级的结果。

BPU 顶层中所有的生成器共有 6 个:

  • npcGen 维护 pc
  • ghistPtrGen 维护全局历史指针
  • ghvBitWriteGens 维护全局历史写入数据
  • foledGhGen 维护折叠历史
  • lastBrNumOHGen 维护上周期最后一个生效的分支指令位置
  • aheadFhObGen 维护分支历史最老位

其中,除了 npcGen 以外,其余生成器都会在本文档中进行介绍。本节中我们着重探讨一下生成器的产生下一次预测的方法。

在代码中你可以看到生成器用类似下面这种方式进行定义:

val npcGen = new PhyPriorityMuxGenerator[UInt]

接下来,代码通过多条语句对生成器的数据来源进行注册:

npcGen.register(true.B, reg ...)
npcGen.register(s1_valid, s1_target, ...)
npcGen.register(s2_redirect, s2_target, ...)
npcGen.register(s3_redirect, s3_target, ...)
npcGen.register(do_redirect.valid, do_redirect.bits.cfiUpdate.target, ...)

每一行被称作一次注册,在一次注册中第一个信号参数是数据有效信号,第二个信号参数包含具体的数据。 生成器的优先级也是按照注册的顺序来决定,越往后优先级越高,因此,同一时刻的优先级从低到高依次为:

  • s0 阻塞的数据
  • 根据 s1 预测结果更新后的数据
  • 根据 s2 预测结果更新后的数据
  • 根据 s3 预测结果更新后的数据
  • BPU 外部重定向中的数据

这样一来,我们就可以在预测结果重定向有效时,避免采用较早流水级的预测结果,而采用纠正后的预测结果。也使得我们可以将外部重定向请求作为最高优先级去处理。

我们可以得出所有生成器产生 s0 信号的方法:在所有数据有效信号中,如果只有一个有效的,则选取它对应的数据,如果有多个数据有效信号生效,则选取优先级最高的数据。

全局分支历史

我们知道,全局分支历史在 BPU 顶层进行维护,维护的策略与 PC 的维护策略一致。即在每个阶段流水级预测结果产生之后,会根据相应信号对全局分支历史进行更新。

顶层为了维护全局分支历史定义了两组信号

  • ghv 存储了全局分支历史(最大长度 256)
  • ghist_ptr 全局分支历史指针,指向全局分支历史当前的位置

s0_pc, s1_pc, s2_pc 一样,BPU 顶层为全局历史指针也维护了每一阶段的信号 s0_ghist_ptr, s1_ghist_ptr, s2_ghist_ptr ,但 ghv 中的内容是位置固定的,我们仅通过 ghist_ptr 来定位当前的全局分支历史从哪里开始。

通过 ghist_ptr 计算当前全局分支历史

ghist_ptr 的使用仅在 BPU 顶层可见,而我们向子预测器传入的,是全局历史寄存器中的数据根据 ghist_ptr 所移位之后的全局分支历史。在子预测器拿到的全局分支历史中,最低位对应全局分支历史的最新位,最高位对应全局分支历史的最老位。

那么是怎样进行移位的呢,我们首先来看一下全局历史是怎样在 ghv 中进行存储的。

|===== ghist =====>| =======>|
n                  ^         0
                   ghist_ptr

如上图所示,序列表示整个 ghv 寄存器,ghist_ptr 指向 ghv 中的某个位置,这个位置代表了全局分支历史的最新位。当需要添加一位新的全局历史记录时,首先将 ghist_ptr 减 1,然后将该位写在其所指向的位置。当 ghist_ptr 减到 0 后,又会循环回来指向最高位,因此会覆盖之前写入的全局分支历史。

但不管怎样,从 ghist_ptr 所指向的位置开始,指针越增加,历史越老。因此,当我们需要计算当前全局分支历史时,只需要将 ghv 寄存器循环右移 ghist_ptr 位即可。

全局分支历史的更新

全局分支历史的更新策略与 pc 更新的策略一致,在每一个流水级都需要根据当前流水级的预测结果生成一个 当前流水级的指针及 ghv 的更新说明,最终都送给相关生成器来处理。

ghv 的更新说明即 用于指导 ghv 寄存器的更新的某些信息。香山 BPU 中维护了两个信息来完成这一职责:

  • ghv_wdata 需要向 ghv 中写入的数据
  • ghv_wens 写入位掩码

最终更新时,只需要将 ghv_wens 所标识的位写入 ghv_wdata 的对应位即可。

因此每个流水级需要负责产生三组信息:ghist_ptr,ghv_wdata, ghv_wens

具体地,预测结果中最多含有两条分支指令,我们只需将实际情况来设置这几个信息即可,举几种情况的例子:

  • 只有第一个槽有效,并且其中条件分支指令被预测为不跳转。则将 ghv_wens 的下一个位置置 0 ,ghv_wens 的对应位置置 1, 同时 ghist_ptr 减一。
  • 两个槽都存放了条件分支指令,第一条被预测为不跳转, 第二条被预测为跳转。此时 ghist_ptr 应该减二,并且其他两个信息应该指示向 ghv 中写入 01。

此处在生成器中只维护了一个 ghv_wdata 信息(通过 ghvBitWriteGens 生成器维护),ghv_wens 并没有通过生成器来维护。这是因为此处使用了一个小技巧,使用了生成器的 ghv_wdata 最终输出的是被选中阶段的结果,而 ghv_wens 将所有阶段的 ghv_wens 进行按位或来使用。

这是基于如下考虑的:

  • 如果较晚的流水线阶段有效。全局历史指针被恢复到较老的位置,即便被早期流水的 ghv_wens 修改了较新位置的历史也没关系。
  • 如果较早的流水线阶段有效。全局历史指针继续向较新的位置更新,而后期流水线会因为 redirect 未生效而不把 ghv_wens 置位。

分支折叠历史

送入预测器的分支折叠历史也是由顶层 BPU 来维护的,BPU 为了缩短折叠历史的更新延迟,维护了很多变量,来支持分支折叠历史的快速更新,我们将会重点介绍这一策略,并介绍每一个变量的作用。

在开始之前,我们先来看一下分支折叠历史是怎样定义的,结构又是怎样的。

分支折叠历史

如果你查看了 BPU 全局接口的文档,你就会知道,子预测器拿到的是一个不同长度位向量的数组,代表了各种长度的折叠历史,而这些折叠历史都是由全局分支历史压缩而成

对于全局分支历史,我们有一个存放全局分支历史的寄存器,长度为 256。为了方便举例,我们假设全局分支历史的长度为 15 位,并且经过移位之后,我们可以拿到一个这样的分支历史:最低位是最新的历史记录,最高位是最老的历史记录。

此时如果我们需要用这 15 位,产生一个 6 位的折叠历史,会使用异或的策略进行压缩,具体过程是这样的:

    h[5]         h[4]       h[3]    h[2]   h[1]   h[0]
    h[11]        h[10]      h[9]    h[8]   h[7]   h[6]
^                                   h[14]  h[13]  h[12]
---------------------------------------------------------------
    h[5]^h[11]   h[4]^h[10]         ...           h[0]^h[6]^h[12]

即将其按照上面的方式排列之后,将每一位上的值进行异或,结果便是求出的长度为 6 的折叠历史。

分支折叠历史更新方法

此时我们想要对这一分支折叠历史进行更新,当我们向全局分支历史插入一位新历史时,是从最低位插入的,也就是说原来的 h[0] 变为了 h[1],如果我们想求此时的分支折叠历史,只需要再进行一遍异或运算。但这样的效率太低了,因为异或的操作有可能变得特别长,我们可以来探寻一下一次更新对分支折叠历史的影响。

上述例子中,插入一位新历史之前,6 位折叠历史的生成是按照下面这种排列生成的

h[5]   h[4]   h[3]  h[2]  h[1]  h[0]
h[11]  h[10]  h[9]  h[8]  h[7]  h[6]
                    h[14] h[13] h[12]

插入一位新历史之后变成了下面这样

h[4]   h[3]   h[2]  h[1]  h[0]  h[new]
h[10]  h[9]   h[8]  h[7]  h[6]  h[5]
           (h[14])  h[13] h[12] h[11]

我们可以发现一些规律

插入前:
h[5]   {h[4]   h[3]  h[2]  h[1]  h[0] }
h[11]  {h[10]  h[9]  h[8]  h[7]  h[6] }
       {             h[14] h[13] h[12]}
插入后:
{h[4]   h[3]   h[2]  h[1]  h[0] } h[new]
{h[10]  h[9]   h[8]  h[7]  h[6] } h[5]
{           (h[14])  h[13] h[12]} h[11]

大括号中的内容发生了整体的左移,h[5] 和 h[11],由最高位变到了最低位。那么表现在压缩后的历史上不就是我们常见的循环左移吗!

但其中有且仅有两个位的值发生了变化,一个是新加入的 h[new],一个是被舍弃掉的 h[14]。h[new] 肯定在第一位,被舍弃的位置也是固定的。因此我们想要完成一次更新,只需要知道 新插入历史的值 和 前一次历史的最老位即可。循环移位后,将这两个位置根据实际情况进行一次修改便可拿到更新后的折叠历史。

更新方法实现

BPU 顶层为了实现这种更新,正是通过维护最老位,这通过两个额外的变量来实现:

  • ahead_fh_oldest_bits 全局分支历史的最老位,还额外往前存储了若干位
  • last_br_num_oh 上一次预测最后一个生效的分支指令在第几个槽

在这里有一处为时序所优化的点,因为当流水级的预测结果出来时,全局历史指针才能通过跳转情况进行更新,等到全局历史指针更新完再来更新最老位会增加时延。因此我们将跳转情况维护起来,等到下一周期用的时候再来用跳转情况更新最老位。

此时的最老位也需要多往前维护几位,因为在使用时,利用跳转情况更新后,前面较新的几位就会变成最老位了。

所以与折叠历史相关的生成器共有三个:foldedGhGen, lastBrNumOhGen, aheadFhObGen

每次折叠历史更新时需要的信息分别是

  • 更新前的折叠历史信息
  • 全局分支历史最老位(ahead_fh_oldest_bits)
  • 上次预测的跳转情况(last_br_num_oh)
  • 本次更新是否有指令跳转
  • 本次更新的跳转情况:最后一个生效的分支指令在第几个槽

每次折叠历史更新时,都需要根据 last_br_num_ohahead_fh_oldest_bits 求出真正的最老位,然后通过最老位与本次更新的跳转情况将其中的若干位进行修改,最后进行循环左移,便完成了更新操作。

流水线控制方法

流水线控制是 BPU 功能的核心,逻辑也最为复杂,BPU 顶层中所有的流水线控制信号如下:

  • s1_valid, s2_valid, s3_valid 表示对应流水数据生效
  • s1_ready, s2_ready, s3_ready 表示对应流水已准备好继续上一流水级的预测
  • s1_component_ready, s2_component_ready, s3_component_ready 表示对应流水子预测器的 ready 情况
  • s0_fire, s1_fire, s2_fire, s3_fire 握手成功信号,表示该流水数据生效,并成功传递给了下一流水
  • s1_flush, s2_flush, s3_flush 表示当前流水是否需要冲刷
  • s2_redirect, s3_redirect 表示当前流水在 fire 的同时,是否预测结果不同,需要产生预测结果重定向

valid, ready 与 fire

我们会逐步来介绍每个信号的作用,首先我们来看 fire 信号,这一信号表示的含义是流水线握手成功,数据成功传给了下一流水。这标志着本周期结束时,本流水级的预测也随之结束,下周期开始时,下一流水级的预测即将开始。

这需要两个条件:

  1. valid 本流水级的数据是有效的。
  2. readycomponent_ready 分别指示了 BPU 顶层与预测器的下一流水级是否就绪。

当这两个信号同时置高时,fire 信号有效,表示握手成功。如果我们单独把一次预测拿出来,那么时序应该是这样的(实际中,大多数时间每个流水线都是一直有效的):

上文中提到的四组信号,除了 component_ready 是由预测器输出,其余信号皆需 BPU 顶层来维护,而最终暴露给子预测器的,只有 fire 一组信号。

我们接下来以 s2 为例分别来看每个信号是如何维护的。

ready 信号

s2_ready := s2_fire || !s2_valid

该赋值语句是一个组合电路赋值,也就是说,s2_ready 信号是与本周期的 s2_fires2_valid 直接相关联的,分为以下两种情况:

  • s2_valid 信号在本周期无效,说明 s2 流水级目前是空的,自然可以接受新的数据,则 s2_ready 有效
  • s2_valid 信号在本周期有效,说明 s2 流水级目前有数据还未传递给下一级,但如果 s2_fire ,那么本周期就会传递给下一级。此时 s2_ready 有效,刚好指示数据可以在下一拍流入。

valid 信号

s2_valid 信号目前为止维护是相对简单的,与 s1_fire 信号和 s2_ready 信号相关。其关系为:

  • s1_fire 有效,说明数据传进来,下一周期 s2_valid 有效。
  • s2_fire 有效,说明数据流出去,下一周期 s2_valid 无效。

fire 信号

fire 信号相对特殊,但对于中间的流水级来说,维护非常简单,例如

s2_fire := s2_valid && s3_components_ready && s3_ready

只需考虑当前流水级的 valid 和下一流水级的 ready 即可。

但对 s0_fire 来说,没有 valid 信号,因此其直接等于 s1_components_ready && s1_ready

对于 s3_fire 来说,没有下一级的 ready 信号,因此其直接等于 s3_valid

加入 flush 和 redirect

我们知道,当流水线出现预测结果不同时,需要产生预测结果重定向信号,并且将之前的流水线清空。flushredirect 正是在做这两项工作。redirect 表示当前流水级是否需要重定向,flush 则表示当前流水级是否需要冲刷。

redirect 信号

s2_redirect 的产生方式如下:

s2_redirect := s2_fire && s2_redirect_s1_last_pred

也就是说,当 s2_fire 时,并且 s2 的预测结果与上一周期保存的 s1 预测结果不同时,这个信号便有效。之后该信号将会连接到子预测器的输入,与 BPU 预测结果的输出,分别指导子预测器和 FTQ 的状态恢复。

flush 信号

flush 信号是用于指导流水线冲刷的,例如 s3 重定向有效时,说明错误的预测结果已经流入流水线, s1 和 s2 此时全都是基于错误的结果来预测的,因此需要进行流水线冲刷,使之前的流水级都暂停工作,等待新的预测结果流入。

具体地,他们之间有如下关系:

 s2_flush := s3_flush || s3_redirect
 s1_flush := s2_flush || s2_redirect

也就是说,某个流水级 redirect 有效,之前的流水级的 flush 全都会被置为有效。那么 flush 具有什么作用呢?答案是指导 valid 信号,如果本周期 valid 信号有效,但 fire 信号未生效,说明错误的数据没有被下一流水取走,此时 flush 有效后,在下一周期 valid 就会立即变为无效,以这种方式来避免错误数据长期存储在流水线中。

但 flush 信号对 valid 信号的影响,也根据每一个流水级的不同而有一定差异。例如

  • s1 流水级。虽然 flush 有效,但是如果此时 s0_fire 有效,说明新数据流入,那么下周期 valid 依然有效。
  • s2 流水级。flush 有效,那么必定下周期不会 valid(因为 s1 也肯定被 flush),此时就可以直接将 valid 置为无效。但还存在一种特殊情况,s2_redirect 发生时,s2_flush 并没有被置为有效,此时如果发生 s1_fire,s1 的错误预测结果也可能流入,此时还需根据 s1_flush 信号来决定 s2_valid 是否有效。

flush 的使用较复杂,更详细的细节还需参考代码进行理解。

重定向恢复逻辑

当 FTQ 发往 BPU 的重定向请求生效时就说明所有流水级的预测结果都是不正确的,此时应该将所有流水级进行冲刷,这可以通过将 s3_flush 置为有效来实现。因此有

 s3_flush := redirect_req.valid

在 BPU 中,重定向请求送入后被延迟一周期才正式使用,因此 s1_valid 的信号也需要对 flush 信号的响应做出一些改变。当重定向请求(延迟前)有效时,s1_valid 下周期立即被置为无效,不需要再去参考 s0_fire 信号了。

此时 npcGen 等生成器也需要直接去采用重定向请求中的数据来生成,这就相当于将 BPU 的状态重定向到出错之前状态的过程。但注意 BPU 默认的重定向等级为 flushAfter ,即重定向请求会对应一条预测错误的指令,而 BPU 会认为这条指令虽然预测错了,但是已经被纠正并且交由后端执行了,因此下一次预测可以直接从下一条指令开始。

所以在重定向恢复时,不仅需要将重定向接口中的信息恢复,还需要将这条预测错误的指令的执行情况也更新到历史中去。

3.2 - uFTB 分支预测器

uFTB 简介

uFTB 是香山所有预测器中第一个预测器,也是其他预测器产生预测结果的基石。uFTB 工作在 s1 阶段,它获取到 s1_pc 后能在本周期内产生预测结果,并在 s1 通道输出,其他通道不予更改。分支指令的位置、指令的跳转目标都是由它来提供,后续预测器也将会根据这一预测结果进行进一步的预测。

而其本质则是一个 FTB 项缓存,其中存储了 FTB 项,基础的预测结果将直接由读出的 FTB 项生成。

因此,在你开始文档的阅读之前,请确保你理解了 FTB 项和含义以及预测结果接口的具体细节。

uFTB 功能简介

  • 缓存FTB项,生成一周期预测结果 uFTB 中维护了一个小型的 FTB 项缓存,拿到 PC 之后,会在一周期之内读出 PC 所对应的 FTB 项,并从 FTB 项生成 s1 阶段预测结果。
  • 维护两比特饱和计数器,提供基础条件分支结果 uFTB 中为 FTB项缓存的每一行都维护了对应的两比特饱和计数器,其方向预测结果会反映在 uFTB 的预测结果输出中。
  • 根据更新请求更新 FTB 缓存和两比特饱和计数器

uFTB 缓存结构

如上所述,uFTB 本质上是一个存储了 FTB 项的小型缓存,其大致结构如下图所示。

在当前版本的香山中,uFTB 共有 32 个缓存行,每个缓存行被称作 FauFTBWay,一个缓存行中可以存储一个 FTB 项。

当 s1 流水有效时,uFTB 会使用 s1_pc 来决定读出 uFTB 缓存的哪一项。缓存是根据 PC 中的 tag 字段来索引,tag 字段被定义为 pc[16: 1],即在 PC 中截取了 16 比特作为标识来匹配缓存中的某一行。

缓存中的每一行,即 FauFTBWay 中的数据请求接口有三项:

  • req_tag 输入 pc 中截取的 tag 标识
  • resp 输出本行所存储的 FTB 项
  • resp_hit 输出指示本行的 FTB 项是否与 req_tag 匹配

uFTB 会将 tag 连接到缓存的每一行的数据请求接口,并根据 resp_hit 信号,选出命中的那一个 FTB 项。后续将会根据这一 FTB 项生成完整预测结果。

两比特饱和计数器

uFTB 中为 FTB 项缓存的每一行都维护了对应的两比特饱和计数器。我们知道,在一个 FTB 项最多可以存储两个条件分支指令,因此每一行对应的两比特饱和计数器也有两个,负责为其中的条件分支指令提供粗略的预测结果。

uFTB 在索引到 FTB 项时,也会索引到对应的两比特饱和计数器。

当预测结果生成时,会根据 FTB 项与其对应的两个两比特饱和计数器中的内容来产生。

预测结果生成

uFTB 通过 s1_pc 索引到对应的 FTB 项以及两个两比饱和计数器之后,需要根据他们产生预测结果。uFTB 产生的预测结果,将在 s1 流水有效时,通过 s1 通道进行输出,对于 s2 以及 s3 通道,uFTB 不予更改。

s1 预测结果中的信号产生方式可参考以下列表:

  • hit FTB表项是否命中

    • 生成方式:FauFTBWay 中的 resp_hit 信号,存在一个有效
  • slot_valids slot有效位,表示 ftb 项中的每个 slot 是否有效

  • targets 每个slot对应的跳转目标地址

  • offsets 每个slot中的指令相对于预测块起始地址的偏移

  • is_jal 预测块内包含jal指令

  • is_jalr 预测块内包含jalr指令

  • is_call 预测块内包含call指令

  • is_ret 预测块内包含ret指令

  • last_may_be_rvi_call 预测结果预测块末尾可能为一个RVI类型的call指令信号

  • is_br_sharing 最后一个slot (tailSlot) 中存储的是条件分支指令信号

    • 生成方式: FTB 项中对应字段导出
  • fallThroughErr FTB项中记录的 pftAddr 有误

    • 生成方式:比较 pftAddr 代表的预测块结束地址是否大于预测块的起始地址,如果小于,则代表出现错误,此信号置为有效。这种情况可能会发生在 pc 索引到错误的 FTB 项的情况。
  • fallThroughAddr 预测块的结束地址

    • 生成方式:如果 fallThroughErr 无效,则根据 pftAddr 生成,否则将其设置为起始地址 + 预测宽度。
  • br_taken_mask 分支预测结果,每个分支(slot)对应一个 bit,表示该分支是否被预测为 taken

    • 生成方式:根据 FTB 项中的 always_taken 字段和两比特饱和计数器指示结果生成。
  • jalr_target 本预测块中的 jalr 的跳转目标

    • 生成方式:FTB 项中 tailSlot 中的跳转目标。

uFTB 更新

uFTB 的更新涉及 FTB 项缓存的更新,以及两比特饱和计数器的更新,而更新的内容都是通过更新接口来获取的。

在 uFTB 预测器中,缓存和两比特饱和计数器的读与写并不冲突,因此我们无需考虑读和更新之间的时序冲突问题,可以将他们看做是独立的两部分。

FTB 缓存更新

FTB 缓存的更新过程很简单,更新通道中已经为我们指定好了 pc 以及新生成的 FTB 项,只需将其写入到缓存的指定位置即可。

FTB 缓存的更新需要两个周期:

  • 在第一周期,通过 update 中的信号计算出如下内容:
    • 更新请求对应的预测块在缓存的哪一行 更新请求中的 pc 截取 tag 后被发送到 FauFTBWay 的更新请求通道,根据每一行所返回的 hit 信号计算
  • 在第二周期,根据第一周期求出的需要更新的位置进行更新,如果无任何一行命中,则 uFTB 会使用 伪 LRU 替换算法 选出需要写入的行。
    • 具体的,更新通道中的 ftb_entry 信号组包含了新 FTB 项的完整信息,将该信息发送到需要更新的某一个缓存行即可。

两位饱和计数器更新

两位饱和计数器的更新则需要结合后续程序的真实执行情况,以及 FTB 项中记录的分支指令信息来进行更新了,而这些信息都可以从更新通道中进行获取。

两位饱和计数器的更新也需要两个周期:

  • 在第一周期,计算出槽中哪些条件分支指令 对应的两位饱和计数器 需要被更新,这需要满足以下条件:
    • 当前分支指令槽有效,并且存放了一条条件分支指令
    • 当前分支指令槽并没有被标识为 always_taken。(因为被标识为 always_taken 后不会采用两位饱和计数器的结果)
    • 当前分支指令槽不在实际发生了跳转的分支指令槽之后。
  • 在第二周期,更新饱和计数器
    • 根据第一周期生成的需要更新的掩码,以及更新通道中的 br_taken_mask 信息进行更新。

接口列表

信号类型 信号位 信号名 信号描述
input clock 输入时钟
input reset 复位信号
input [35:0] io_reset_vector 用于reset时,reset s1_pc_dup_0 提供的值
input [40:0] io_in_bits_s0_pc_0 输入位s0_pc 的 第0个复制
input [40:0] io_in_bits_s0_pc_1 同上 第1个
input [40:0] io_in_bits_s0_pc_2 同上 第2个
input [40:0] io_in_bits_s0_pc_3 同上 第3个
output [40:0] io_out_s1_pc_0 输出s1_pc 的 第0个复制
output [40:0] io_out_s1_pc_1 同上 第1个
output [40:0] io_out_s1_pc_2 同上 第2个
output [40:0] io_out_s1_pc_3 同上 第3个
output io_out_s1_full_pred_0_br_taken_mask_0 solt 0 是否被预测为 always taken
output io_out_s1_full_pred_0_br_taken_mask_1 solt 1 是否被预测为 always taken
output io_out_s1_full_pred_0_slot_valids_0 solt 0 是否启用
output io_out_s1_full_pred_0_slot_valids_1 solt 1 是否启用
output [40:0] io_out_s1_full_pred_0_targets_0 solt 0 对应的跳转目标地址
output [40:0] io_out_s1_full_pred_0_targets_1 solt 1 对应的跳转目标地址
output [3:0] io_out_s1_full_pred_0_offsets_0 solt 0 中分支指令相对于地址块起始pc的偏移
output [3:0] io_out_s1_full_pred_0_offsets_1 solt 1 中分支指令相对于地址块起始pc的偏移
output [40:0] io_out_s1_full_pred_0_fallThroughAddr 预测块的结束地址
output io_out_s1_full_pred_0_is_br_sharing solt 1(无条件跳转)是否被共享为有条件跳转指令
output io_out_s1_full_pred_0_hit
output io_out_s1_full_pred_1_br_taken_mask_0 类似 io_out_s1_pc_1 io_out_s1_full_pred_0的复制
output io_out_s1_full_pred_1_br_taken_mask_1
output io_out_s1_full_pred_1_slot_valids_0
output io_out_s1_full_pred_1_slot_valids_1
output [40:0] io_out_s1_full_pred_1_targets_0
output [40:0] io_out_s1_full_pred_1_targets_1
output [3:0] io_out_s1_full_pred_1_offsets_0
output [3:0] io_out_s1_full_pred_1_offsets_1
output [40:0] io_out_s1_full_pred_1_fallThroughAddr
output io_out_s1_full_pred_1_is_br_sharing
output io_out_s1_full_pred_1_hit
output io_out_s1_full_pred_2_br_taken_mask_0 同上
output io_out_s1_full_pred_2_br_taken_mask_1
output io_out_s1_full_pred_2_slot_valids_0
output io_out_s1_full_pred_2_slot_valids_1
output [40:0] io_out_s1_full_pred_2_targets_0
output [40:0] io_out_s1_full_pred_2_targets_1
output [3:0] io_out_s1_full_pred_2_offsets_0
output [3:0] io_out_s1_full_pred_2_offsets_1
output [40:0] io_out_s1_full_pred_2_fallThroughAddr
output io_out_s1_full_pred_2_is_br_sharing
output io_out_s1_full_pred_2_hit
output io_out_s1_full_pred_3_br_taken_mask_0 同上
output io_out_s1_full_pred_3_br_taken_mask_1
output io_out_s1_full_pred_3_slot_valids_0
output io_out_s1_full_pred_3_slot_valids_1
output [40:0] io_out_s1_full_pred_3_targets_0
output [40:0] io_out_s1_full_pred_3_targets_1
output [3:0] io_out_s1_full_pred_3_offsets_0
output [3:0] io_out_s1_full_pred_3_offsets_1
output [40:0] io_out_s1_full_pred_3_fallThroughAddr
output io_out_s1_full_pred_3_fallThroughErr
output io_out_s1_full_pred_3_is_br_sharing
output io_out_s1_full_pred_3_hit
output [222:0] io_out_last_stage_meta 输出最后阶段的元信息 io_out_last_stage_meta = {213’h0, resp_meta_pred_way_r_1, resp_meta_hit_r_1}
input io_ctrl_ubtb_enable 控制ubtb是否启用
input io_s0_fire_0 输入s0_fire_0,与 io_out_s1_pc_0 <= io_in_bits_s0_pc_0 的时钟门控相关
input io_s0_fire_1 输入s0_fire_1
input io_s0_fire_2 输入s0_fire_2
input io_s0_fire_3 输入s0_fire_3
input io_s1_fire_0 输入s1_fire_0
input io_s2_fire_0 输入s2_fire_0
input io_update_valid 更新有效性
input [40:0] io_update_bits_pc 传回的预测块pc(用于指示更新的预测块)
input [3:0] io_update_bits_ftb_entry_brSlots_0_offset solt 0 中分支指令相对于地址块起始pc的偏移
input [11:0] io_update_bits_ftb_entry_brSlots_0_lower 跳转目标地址的低位
input [1:0] io_update_bits_ftb_entry_brSlots_0_tarStat 跳转后的 pc 高位是否进退位
input io_update_bits_ftb_entry_brSlots_0_valid 是否启用
input [3:0] io_update_bits_ftb_entry_tailSlot_offset solt 1 中分支指令相对于地址块起始pc的偏移
input [19:0] io_update_bits_ftb_entry_tailSlot_lower 跳转目标地址的低位
input [1:0] io_update_bits_ftb_entry_tailSlot_tarStat 跳转后的 pc 高位是否进退位
input io_update_bits_ftb_entry_tailSlot_sharing 无条件跳转指令槽中存储条件分支指令
input io_update_bits_ftb_entry_tailSlot_valid 是否启用
input [3:0] io_update_bits_ftb_entry_pftAddr Partial Fallthrough Addr 如果预测块中没有跳转,那么程序将会顺序执行到达的地址,预测块的结束地址。
input io_update_bits_ftb_entry_carry pc+pft时是否产生进位
input io_update_bits_ftb_entry_always_taken_0 是否预测为总是跳转
input io_update_bits_ftb_entry_always_taken_1 是否预测为总是跳转
input io_update_bits_br_taken_mask_0 是否跳转
input io_update_bits_br_taken_mask_1 是否跳转

3.3 - TAGE-SC 分支预测器

功能介绍

TAGE-SC 是南湖架构条件分支的主预测器,属于精确预测器(Accurate Predictor,简称 APD)。TAGE-SC 可以看作两个功能相对独立的组件:预测部分 TAGE 和 校验部分 SC。

  • 标记几何历史长度预测器 TAGE (Tagged Geometric History Length Predictor)利用历史长度不同的多个预测表,可以挖掘极长的分支历史信息。TAGE功能是预测一个跳转指令是跳转(Taken),还是不跳转(不Taken), 它由一个基预测表和多个历史表组成,首先通过多个历史表进行分支预测,如果没有预测结果,则再采用基础预测表的预测结果。
  • SC (Statistical Corrector) 是统计校正器。当 SC 会参考 TAGE 的预测结果和统计偏向的结果。并根据这两个结果,矫正最终的预测结果。

在昆明湖中,由于每个预测块,最多可以有2条跳转指令,因此TAGE在每次预测最多同时预测 2 条条件分支指令。在访问 TAGE 的各个历史表时,用预测块的起始地址作为 PC,同时取出两个预测结果,并基于相同的全局历史进行预测。

昆明湖Tage分支预测器

基本功能

img

TAGE预测器的核心思路是提供不同历史长度的预测结果,并选择最合适的结果进行反馈。在TAGE预测器中,一共有 1+N 个表历史记录表,N 为可配置选项。以昆明湖为例,N=4。

基于 T0 表的预测器为基础预测器。在预测时,直接通过预测块的 PC 地址在 T0 表中查找到该地址对应的 “2 bit 饱和计数器表示的跳转历史信息” ,然后根据历史信息做出预测结果。T0表的表项只有2bit,所以能记录的历史状态有限。

对于 T0 以外的表,我们用 Tn 进行表示。在查表时除了 PC 外,还需要用到全局跳转历史信息H进行查找。查找命中后,根据 “3bit饱和预测器” 进行跳转或者不跳转的预测。Tn 表的n越大,其用到的H中的位数越多。

对于每一次预测,TAGE 从所有 Tn 命中表项中选择全局跳转历史最长的表项。

  1. 如果该表项存在,且预测结果置信度高就用该项作为最终预测结果。
  2. 如果置信度低,就通过另外一个内部计数器来判断选择该项还是 T0 作为最后预测。

在实现上,为了节约空间,检索 Tn 表时,输入的跳转历史信息H需要进行压缩,该过程也称为历史折叠

各预测表的表项包含如下元素:

  1. T0 表项通过 pc 直接索引
    1. 2 bit pred 无符号饱和计数器(给出预测方向和信心强度)
  2. Tn 表项通过 异或 pc 与 折叠全局历史 索引
    1. 1 bit valid 有效位
    2. 3 bit pred 无符号饱和计数器
    3. 8 bit tag (基于Hash校验是否命中,而非巧合)
    4. 1 bit useful 用于控制有效期

对于一个预测块,有可能所有的表都会产生预测结果,此时就需要进行选择。如上图所示,一般情况下,Tn表的编号越大,优先级越高。

流水线

TAGE 内含两个流水级,第一级计算索引,第二级通过索引读出SRAM表中的结果

  1. 第0拍,s0:第一个流水级的输入,一般是pc和折叠历史。

第一流水级运行:计算index。通过寄存器输出到s1

  1. 第1拍,s1:第二个流水级的输入,为第一级阶段计算好的index等数据。

第二流水级运行:访存SRAM,读取预测结果。通过寄存器输出到s2

  1. 第2拍,s2:实际预测结果。TAGE用了2拍进行预测,在2拍之后的第3拍预测结果就绪可用。

数据结构

  1. 在昆明湖的实现中,T0与Tn的表结构如下所示:

    1. 预测器 作用 表项构成 项数
      基准预测器T0 用于在其他预测器的预测结果都无效时输出预测结果 2 bit ctr 饱和计数器最高位决定跳转方向 2路各2048项,每路对于一条分支指令
      预测表T1-T4 对每个预测块的输入,所有Tn表都进行预测,在所有预测有效的结果中,选择历史记录最长的结果作为最后预测结果。历史记录长度由输入的H决定 1 bit valid 有效位 3 bit ctr 饱和计数器8 bit tag 校验命中1 bit us 作为usefulness计数器 4096项、奇数项对应第一条分支指令,偶数项对应第二条分支指令

对于每个表Tn,在查询时,其输入“全局分支历史数据H”的长度是不同的。假如总的预测历史长度为S,Tn、Tn+1可能用S的低x,低y位 (越低的位是越新的历史)作为查询输入。一般情况下,Tn表的n越大,其用到的历史信息越长,即 x<y 。

在进行 Tn 表的查询时,由于历史数据H进行了“压缩”,可能导致一个PC1^H1的结果与另一个PC2^H2的结果相同(类似Hash表的Hash冲突),进而导致索引到无效数据(预测PC1索引到预测PC2的数据)。所以TAGE为每个表提供了一个tag标识,昆明湖中采用了8bit tag,来减小冲突发生的概率,tag的计算方法和查询的索引方法不同,只有当tag计算相同时查询结果才有效

在Tn表项中,除了饱和计数器ctr和tag外,还有1bit的usefulness计数器,该计数器为0时,作弱表项,表示可以当作该项可以被重新分配当作他用;不为0时,作强表项,该项不能被分配当作他用。

为了尽力避免所有表项全为1,无法分配新表项的情况发生,TAGE预期使用计数器bankTickCtrs 对所有usefulness进行清零操作。

T0,Tn表的检索方法

  • 对于T0表,在查找时用 PC[11:1] 位进行索引2048表项,因此对于T0来说,不可能存在检索不到的情况。
  • 对于表Tn,除了 PC[11:1] 外,还需要基于全局分支历史进行检索。在昆明湖中,顶层分支预测器维护了一个256bit的全局历史记录GH,可以根据子预测器需要将GH的最近n位历史信息,按目标位数x进行折叠。 即把n,以长度x为基本单元,拆分成ceil(x/n)个长为x的数,然后按位进行异或得到。通常用FH(Folded History)表示,具体流程请参考 分支折叠历史章节。TAGE预测器查找Tn的表项时,用到了index和tag,他们的计算公式如下表所示:
计算方法 公式
index = FH ^ ((pc»1)低位)
tag = FH1 ^ FH2 ^ ((pc»1)低位)

其中FH、FH1、FH2表示按一定规范折叠后的全局分支历史。对于Tn而言,其FH、FH1、FH2都有各自的对于折叠位数,不一定相同。在昆明湖实现中,T0和Tn表的配置如下:

表名称 FH长度 FH1长度 FH2长度 最近历史长度(用到GH中的位数)
T1 8比特 8比特 7比特 低8位,即把最新8位历史,折叠成FH、FH1、FH2
T2 11比特 8比特 7比特 低13位,即把最新13位历史,折叠成FH、FH1、FH2
T3 11比特 8比特 7比特 低32位,即把最新32位历史,折叠成FH、FH1、FH2
T4 11比特 8比特 7比特 低119位,即把最新119位历史,折叠成FH、FH1、FH2

注:pc»1是因为使用了RISC-C拓展,2Byte对齐,而PC本身已经省略了1B对齐,所以只用1b

替代预测器

由于 Tn 表使用饱和计数器进行预测,因此其输出结果可能存在“信心不足”的情况。例如在昆明湖中,对3比特饱和计数器,100、011时都是弱预测。为了为该状态提供更多的选择参考,TAGE 预测器还提供了一个“替代预测器”机制,他的作用是告诉 TAGE 在 Tn 表预测的结果 “信心不足” 时,是该选择 Tn 的预测结果,还是选择 T0 的预测结果。

在昆明湖的具体实现中,“替代预测器” 基于 useAltOnNaCtrs 的寄存器组实现。它由两路128个 4-bit饱和计数器 构成,每个计数器都被初始化为 0b1000。在TAGE进行预测时,使用 PC(7,1) 进行索引得到对应的饱和计数器。如果该计数器的值大于等于预设值,且Tn预测结果信心不高,则选择T0的结果,否则选择Tn的结果。

预测过程

综上所述,昆明湖中TAGE预测器的预测步骤如下:

  1. 并行索引 T0 表以及 Tn 表,根据命中结果选择使用哪一个表给出预测结果
    1. 如果有命中到tag匹配的Tn表,由历史记录最长Tn表的ctr饱和计数器给出潜在预测结果。
    2. 如果没有命中Tn表,则由T0表的饱和计数器给出最终预测结果。
  2. 如果匹配到的Tn表的潜在预测结果为弱预测(100,011),且替代预测中PC对应4-bit计数器的值大于等于阈值,则采用T0表的结果为最终结果,否则采用Tn表的预测结果为最终预测结果。

训练过程

由于TAGE的预测过程用到了很多计数器和tag,他们需要根据一定的规则进行更新,该过程称为训练。该训练过程发生在BPU的update阶段,该阶段会输入PC,分支历史、以及否预测正确等信息。香山昆明湖的分支预测进行训练的流程根据不同情况,分为如下几个步骤:

  1. T0作为最终使用的预测结果时更新:发生跳转(即taken)则 pc 索引的 ctr 饱和计数器 +1,否则 -1
  2. 只有T0命中时,进行如下操作
    1. T0预测正确则不额外更新
    2. T0预测错误则尝试随机在Tn中的某个表申请一个新表项 申请新表项时需要对应index位置的原表项useful为0 新表项默认是弱预测,useful为0,并设置其tag为计算出来的新tag
  3. T0和Tn同时命中时,进行如下操作
    1. Tn 始终更新:taken 则 pc 索引的 ctr 饱和计数器 +1,否则 -1 需要注意,”命中“ 表示 index 索引到的表项的 tag 要和计算出的 tag 匹配
    2. T0和Tn结果相同
      1. 预测正确则不额外更新
      2. 预测错误则尝试在比Tn对应历史更长的表中随机申请一个新表项 申请新表项时需要对应index位置的原表项useful为0 新表项默认是弱预测,useful为0,tag设置为用新历史信息计算出来的tag
    3. T0和Tn结果不同
      1. Tn正确则表项 useful +1
        1. 若结果还同时为弱预测,则选用T0的替代预测计数器 -1
      2. Tn错误则表项 useful -1,同时如 3.2.2 在更长历史表中申请新表项
        1. 若结果还同时为弱预测,则选用T0的替代预测计数器 +1
  4. 每次需要分配表时,进行动态重置usefulness标志位
    1. 使用 7bit 的 bankTickCtrs 寄存器,并计算
      1. 可分配的表数 a(历史长度比当前更长,且对应索引的useful为 0 )
      2. 不可分配的表数 b(历史长度比当前更长,且对应索引的useful 不为 0 )
    2. 更新 bankTickCtrs += Δ(饱和计数), Δ = b - a
    3. 当 bankTickCtrs当为最大值时, 重置所有useful=0

昆明湖SC分支预测器

基本功能介绍

SC(Statistics counter)分支预测器是一种基于历史统计信息的分支预测器。

与TAGE类似,在SC中通常有多个表Tn,他们对应了不同历史长度的跳转统计。不同点在于,在预测按PC检索时,每个表Tn都会命中,然后SC把每个命中的表项进行求和,计算总的“饱和计数器”跳转信息,最后根据总的跳转信息判断是否跳转。一般情况下,SC采用“有符号饱和计数器”,计数器值大于0时表示跳转,小于0时表示不跳转。计数器的绝对值越大,表示其预测置信度越高。

在SC预测器中,SC也由多个表组成(例如T1,T2,T3,T4),但相对TAGE预测器少了基础预测表T0。SC中Tn表有6 bit的有符号饱和计数器。SC表的索引方式如下:

计算方式
Index = (FH) ^ ((pc»1)低位)

对应每个表,其表项数和用到的折叠历史长度如下:

表名 表项数 FH长度 折叠历史范围
T1 512 0 不折叠
T2 512 4 把历史信息的低4位,折叠成FH
T3 512 8 把历史信息的低10位,折叠成FH
T4 512 8 把历史信息的低16位,折叠成FH

在计算统计预测结果时,总得统计结果计算公式如下:

$$scCtrSum=\sum_{i=0}^{i<4}( (ctr_{sc} << 1) +1)$$

其中ctr_sc表示每个表的有符号饱和计数器。对其进行左移加一是进行权重调整。累加后的scCtrSum就是SC的最终预测结果,如果为该值大于零,则预测跳转,小于零则预测不跳转。其绝对值越大,表示预测置信度越高。

典型数据转换结果如下(在计算时为了不溢出,扩展到了9位):

  1. 均为 6b100000(强不跳转) ,求得 9b100000100,值为 -252
  2. 均为 6b011111(强跳转),求得 9b011111100,值为 252
  3. 均为 6b000000(弱跳转),求得 9b000000100,值为 4
  4. 均为 6b111111(弱不跳转),求得 9b111111100,值为 -4

预测过程

  1. 通过PC和历史信息结算表Tn的索引 Index
  2. 根据Index查询,得到所有表对应的饱和计数器
  3. 所有所有表查询到的饱和计数器进行求和得到最终预测结果(大于0跳转,小于0不跳转)

训练过程

在update阶段进行饱和计数器的更新。

  1. 如果PC对应的真实指令跳转,这所有表中对应的饱和计数器+1
  2. 如果PC对应的真实指令不跳转,这所有表中对应的饱和计数器-1

昆明湖TAGE-SC分支预测器

有了TAGE为何还需要SC

一些应用上,一些分支行为与分支历史或路径相关性较弱,表现出一个统计上的预测偏向性。对于这些分支,相比基于历史的分支预测,使用计数器捕捉统计偏向的方法更为有效。

TAGE在预测与历史非常相关的分支时非常有效,但对有统计偏向的分支则支持不佳。例如只对一个方向有小偏差,但与历史路径没有强相关性的分支。为了避免该问题,可以在传统TAGE预测器上增加SC预测器。

TAGE-SC功能介绍

在昆明湖 TAGE-SC 预测器中,会同时得到 TAGE 和 SC 的预测结果 P1 和 P2,然后对他们的结果进行累加 P = P1+P2 ,如果 P 的绝对值大于 8bit 的阈值 sc_bank_thres,则采用预测器结果 P,否则则采用 P1 作为最终预测结果。

为了进行动态自适应,阈值sc_thres是需要是动态变化的。为此在实现上TAGE-SC使用了一个5bit的sc_bank_ctr计数器对阈值sc_bank_thres进行调整。另外,由于昆明湖支持同时预测2条分支指令,因此阈值寄存器和对应的控制计数器也是两份。

流水线

TAGE-SC 内含 3 个流水级,TAGE的2级流水已经介绍过,SC部分的流水线如下:

  1. 第0拍,s0 读入 pc 和折叠历史

第一流水级:通过pc和FH计算出index,并输出到s0_idx寄存器

  1. 第1拍,s1 从s0寄存器读出s0_idx作为s1_idx

第二流水级:找到SCTable中s1_idx对应的计数器数据,输出到s1_scResps寄存器

  1. 第2拍,s2 从s1寄存器读出s1_scResps作为s2_scResps

第三流水级:根据s2_scResps选择是否需要反转预测结果,输出到s2_disagree寄存器

  1. 第3拍,s3 从s2_disagree中读出结果,作为s3_disagree输出

预测过程

在TAGE-SC预测时,TAGE的预测结果P1用tage_ctr表示,SC的预测结果P2用scCtrSum表示。在预测时,分为如下四步。

  1. 执行SC预测器,得到预测结果scCtrSum

  2. 并行得到TAGE预测器的预测结果 tage_ctr。

    1. 由于TAGE的预测结果是无符号饱和计数器,而SC的预测结果是有符号饱和计数器,如果对他们进行相加,需要进行数据转换。

    2. 昆明湖在实现上,采用了对TAGE的结果进行转换。转换后的结果用 tageCtrCentered 表示,具体转换过程如下:

    $$tageCtrCentered=((((ctr_{tage} -4)<<1)+1)<<3) $$
    1. 3比特的无符号饱和计数器转换为8比特的有符号饱和计数器结果举例如下所示:

      • 3b100 弱跳转 => 8b00001000 = 8

      • 3b011 弱不跳转 => 8b11111000 = -8

      • 3b111 强跳转 => 8b00111000 = 56

      • 3b000 强不跳转 => 8b11001000 = -56

  3. 对TAGE和SC的预测结果进行相加得到最终预测结果P,在实现上用totalSum进行表示。

$$totalSum = scCtrSum + tageCtrCentered$$
  1. 根据 totalSum 以及 sc_bank_thres 决定最终预测方向

    1. totalSum > 0 且绝对值超过阈值则跳转: 如果 scCtrSum > sc_bank_thres - tageCtrCentered 也可以理解成 totalSum > sc_bank_thres 上面的写法可以降低最大位宽(把保证不溢出需要10bit变成9bit)
    2. totalSum < 0 且绝对值超过阈值则不跳转: 如果 scCtrSum < -sc_bank_thres - tageCtrCentered 也可以理解成 |totalSum| > sc_bank_thres

训练过程

在对 TAGE 和 SC 进行组合后,TAGE-SC 添加了 sc_bank_ctr 计数器用来控制阈值sc_bank_thres。因此在训练时,除了 TAGE 和 SC 本身的训练外,还需要对新增加的计数器进行更新。

在update阶段,其具体更新流程如下:

  1. TAGE-SC采用了预测结果P(即TAGE+SC后的预测结果),如果 |totalSum| 在 [sc_bank_thres -4, sc_bank_thres -2] 的范围内,则对阈值相关寄存器组进行更新
    1. 更新 sc_bank_ctr,饱和计数 若预测正确,则 sc_bank_ctr +=1 若预测错误,则 sc_bank_ctr -=1
    2. 更新 sc_bank_thres ,受限制的饱和运算,sc_bank_ctr 更新后的值已达 0b11111 且 sc_bank_thres <= 31,则 sc_bank_thres +=2sc_bank_ctr 更新后的值为 0 且 sc_bank_thres >=6,则 sc_bank_thres -=2 其余情况thres不变。
    3. sc_bank_thres 更新判断结束后,会对 sc_bank_ctr 再做一次判断 若更新后的sc_bank_ctr若为0b11111或0,则thres_ctr会被置回初始值0b10000。
  2. TAGE-SC采用了预测结果P1(即TAGE的预测结果)不进行任何操作

接口列表

TageSC

信号类型 信号宽度 信号名 信号描述
input * clock
input * reset
input *[35:0] io_reset_vector 用于reset时,reset s1_pc_dup_0 提供的值
input *[40:0] io_in_bits_s0_pc_0 复制的s0_pc的dup数组的第1个,给顶层BPU的PC
input *[40:0] io_in_bits_s0_pc_1 复制的s0_pc第2个,给Tage的PC
input *[40:0] io_in_bits_s0_pc_3 复制的s0_pc的第4个,给SC的PC
input *[10:0] io_in_bits_folded_hist_1_hist_17_folded_hist TageTable 2 用到的11bits 折叠历史 从多长历史范围折叠到11bit见前文所述的表 注意TageTable下标+1,此处 T2 是前文 T3
input *[10:0] io_in_bits_folded_hist_1_hist_16_folded_hist TageTable 3 用到的11bits 折叠历史
input *[6:0] io_in_bits_folded_hist_1_hist_15_folded_hist TageTable 1 用到的7bits 折叠历史
input *[7:0] io_in_bits_folded_hist_1_hist_14_folded_hist TageTable 0 用到的8bits 折叠历史
input *[6:0] io_in_bits_folded_hist_1_hist_9_folded_hist TageTable 2 用到的7bits 折叠历史
input *[7:0] io_in_bits_folded_hist_1_hist_8_folded_hist TageTable 3 用到的8bits 折叠历史
input *[6:0] io_in_bits_folded_hist_1_hist_7_folded_hist TageTable 0 用到的7bits 折叠历史
input *[6:0] io_in_bits_folded_hist_1_hist_5_folded_hist TageTable 3 用到的7bits 折叠历史
input *[7:0] io_in_bits_folded_hist_1_hist_4_folded_hist TageTable 1 用到的8bits 折叠历史
input *[7:0] io_in_bits_folded_hist_1_hist_3_folded_hist TageTable 2 用到的8bits 折叠历史
input *[10:0] io_in_bits_folded_hist_1_hist_1_folded_hist TageTable 1 用到的11bits 折叠历史
input *[3:0] io_in_bits_folded_hist_3_hist_12_folded_hist SCTable 1 用到的 4bit 折叠历史
input *[7:0] io_in_bits_folded_hist_3_hist_11_folded_hist SCTable 2 用到的 8bit 折叠历史
input *[7:0] io_in_bits_folded_hist_3_hist_2_folded_hist SCTable 3 用到的 8bit 折叠历史
output * io_out_s2_full_pred_0_br_taken_mask_0 io_out_s2_full_pred_{i}br_taken_mask{j} Tage 在 s2流水级输出的,复制4份 预测块中第 j 条分支指令TAGE预测结果 这里不该叫mask吧
output * io_out_s2_full_pred_0_br_taken_mask_1
output * io_out_s2_full_pred_1_br_taken_mask_0
output * io_out_s2_full_pred_1_br_taken_mask_1
output * io_out_s2_full_pred_2_br_taken_mask_0
output * io_out_s2_full_pred_2_br_taken_mask_1
output * io_out_s2_full_pred_3_br_taken_mask_0
output * io_out_s2_full_pred_3_br_taken_mask_1
output * io_out_s3_full_pred_0_br_taken_mask_0 io_out_s3_full_pred_{i}br_taken_mask{j} Tage 在 s3流水级输出的,复制4份 预测块中第 j 条分支指令SC预测结果
output * io_out_s3_full_pred_0_br_taken_mask_1
output * io_out_s3_full_pred_1_br_taken_mask_0
output * io_out_s3_full_pred_1_br_taken_mask_1
output * io_out_s3_full_pred_2_br_taken_mask_0
output * io_out_s3_full_pred_2_br_taken_mask_1
output * io_out_s3_full_pred_3_br_taken_mask_0
output * io_out_s3_full_pred_3_br_taken_mask_1
output *[222:0] io_out_last_stage_meta 见附表
input * io_ctrl_tage_enable
input * io_ctrl_sc_enable
input * io_s0_fire_0 s0 阶段流水线控制 相同信号复制多份,0给BPU,1给Tage,3给SC
input * io_s0_fire_1
input * io_s0_fire_3
input * io_s1_fire_0 s1 阶段流水线控制
input * io_s1_fire_1
input * io_s1_fire_2
input * io_s1_fire_3
input * io_s2_fire_0 s2 阶段流水线控制
input * io_s2_fire_1
input * io_s2_fire_2
input * io_s2_fire_3
output * io_s1_ready tage的所有表,可以执行读取结果的操作
input * io_update_valid 从FTQ发向BPU的后端执行结果(更新信号)是否有效
input *[40:0] io_update_bits_pc (后端执行过的)预测块的PC
input *[10:0] io_update_bits_spec_info_folded_hist_hist_17_folded_hist TageTable 2 用到的11bits 折叠历史 预测时使用的分支历史结果,没有更新,转了一圈回来了
input *[10:0] io_update_bits_spec_info_folded_hist_hist_16_folded_hist TageTable 3 用到的11bits 折叠历史
input *[6:0] io_update_bits_spec_info_folded_hist_hist_15_folded_hist TageTable 1 用到的7bits 折叠历史
input *[7:0] io_update_bits_spec_info_folded_hist_hist_14_folded_hist TageTable 0 用到的8bits 折叠历史
input *[3:0] io_update_bits_spec_info_folded_hist_hist_12_folded_hist SCTable 1 用到的 4bit 折叠历史
input *[7:0] io_update_bits_spec_info_folded_hist_hist_11_folded_hist SCTable 2 用到的 8bit 折叠历史
input *[6:0] io_update_bits_spec_info_folded_hist_hist_9_folded_hist TageTable 2 用到的7bits 折叠历史
input *[7:0] io_update_bits_spec_info_folded_hist_hist_8_folded_hist TageTable 3 用到的8bits 折叠历史
input *[6:0] io_update_bits_spec_info_folded_hist_hist_7_folded_hist TageTable 0 用到的7bits 折叠历史
input *[6:0] io_update_bits_spec_info_folded_hist_hist_5_folded_hist TageTable 3 用到的7bits 折叠历史
input *[7:0] io_update_bits_spec_info_folded_hist_hist_4_folded_hist TageTable 1 用到的8bits 折叠历史
input *[7:0] io_update_bits_spec_info_folded_hist_hist_3_folded_hist TageTable 2 用到的8bits 折叠历史
input *[7:0] io_update_bits_spec_info_folded_hist_hist_2_folded_hist SCTable 3 用到的 8bit 折叠历史
input *[10:0] io_update_bits_spec_info_folded_hist_hist_1_folded_hist TageTable 1 用到的11bits 折叠历史
input * io_update_bits_ftb_entry_brSlots_0_valid FTB 表项的第一个slot是否有效(存储了跳转指令)
input * io_update_bits_ftb_entry_tailSlot_sharing FTB 表项的最后一个slot是否存储了条件分支而非无条件跳转
input * io_update_bits_ftb_entry_tailSlot_valid FTB 表项的最后一个slot是否有效
input * io_update_bits_ftb_entry_always_taken_0 历史上slot 0 指令总是跳转
input * io_update_bits_ftb_entry_always_taken_1 历史上slot 1 指令总是跳转
input * io_update_bits_br_taken_mask_0 solt 0 是否 taken
input * io_update_bits_br_taken_mask_1 solt 1 是否 taken
input * io_update_bits_mispred_mask_0 solt 0 是否预测正确
input * io_update_bits_mispred_mask_1 solt 1 是否预测正确
input *[222:0] io_update_bits_meta 见附表

io_out_last_stage_meta

需要设计参与优化!

信号类型 信号位 信号名 信号描述
output [218:88] 0 占位,全为0,传递到composer时会忽略
87 resp_meta_providers_1_valid_r
[86:85] resp_meta_providers_1_bits_r
84 resp_meta_providers_0_valid_r
[83:82] resp_meta_providers_0_bits_r
[81:79] resp_meta_providerResps_1_r_ctr
78 resp_meta_providerResps_1_r_u
77 resp_meta_providerResps_1_r_unconf
[76:74] resp_meta_providerResps_0_r_ctr
73 resp_meta_providerResps_0_r_u
72 resp_meta_providerResps_0_r_unconf
71 resp_meta_altUsed_1_r
70 resp_meta_altUsed_0_r
69 resp_meta_altDiffers_1_r
68 resp_meta_altDiffers_0_r
[67:66] resp_meta_basecnts_1_r
[65:64] resp_meta_basecnts_0_r
[63:60] resp_meta_allocates_1_r
[59:56] resp_meta_allocates_0_r
55 resp_meta_takens_1_r
54 resp_meta_takens_0_r
53 resp_meta_scMeta_tageTakens_1_r
52 resp_meta_scMeta_tageTakens_0_r
51 resp_meta_scMeta_scUsed_1_r
50 resp_meta_scMeta_scUsed_0_r
49 resp_meta_scMeta_scPreds_1_r
48 resp_meta_scMeta_scPreds_0_r
[47:42] r_1_3 scMeta(预测时的状态)中第2路的第4个sc_ctr的值
[41:36] r_1_2 scMeta中第2路的第3个sc_ctr的值
[35:30] r_1_1 scMeta中第2路的第2个sc_ctr的值
[29:24] r_1_0 scMeta中第2路的第1个sc_ctr的值
[23:18] r_3 scMeta中第1路的第4个sc_ctr的值
[17:12] r_2 scMeta中第1路的第3个sc_ctr的值
[11:6] r_1 scMeta中第1路的第2个sc_ctr的值
[5:0] r_0 scMeta中第1路的第1个sc_ctr的值

io_update_bits_meta

信号类型 信号位 信号名 信号描述
input [218:94] FTB, ITAGE, RAS 模块传给 FTQ 的 META 信息,忽略
[93:6] io_out_last_stage_meta[87:0] 偏移 6bit 后的结果 TAGE 输出给 FTQ 的 META
[5:0] uFTB 输出给 FTQ 的 META

3.4 - FTB 分支预测器

FTB 简介

FTB 是香山 BPU 的第三个子预测器,它也能一并获取到 uFTB 和 TAGE-SC 的输出。在 FTB 的输入接口中,s1 通道含有 uFTB 的基础预测结果,s2 通道和 s3 通道中仅有 br_taken_mask 一组信号被 TAGE-SC 填充,并无 FTB 项生成的基础预测结果。FTB 的工作便是为 s2 和 s3 通道提供基础预测结果。

FTB 在功能和结构上都与 uFTB 类似,其主要区别就是 FTB 能够容纳更多的 FTB 项,并且 FTB 的预测结果是在 s2 与 s3 通道输出。正是由于容量大,其读出的速度上会比 uFTB 慢,无法被放置在第一周期产生预测结果,但大容量也使它能够获得更加精准的预测结果。

uFTB 功能简介

  • 缓存更多 FTB 项,为 s2 和 s3 通道提供基础预测结果。 FTB 预测器的本质是一个较大容量的存储器,其会根据当前预测的 PC 读出对应的 FTB 项,并在 s2 阶段产出预测结果。与此同时该 FTB 项还会被再保存一个周期,生成 s3 阶段预测结果。生成结果需要注意的点是要考虑到上一预测器输入的 br_taken_mask 字段,避免在生成时丢失。
  • 根据更新请求,更新存储中的 FTB 项。

FTB 存储结构

FTB 预测器中 FTB 项被放置在了一个专门的存储结构中,叫做 FTBBank ,在进一步查看 FTBBank 的结构之前,我们先来看一下 FTBBank 是怎样使用的。

FTB 读请求

FTBBank 的读请求接口如下:

  • req_pc 请求的 pc
    • 接口类型:Flipped(DecoupledIO(UInt(VAddrBits.W)))
  • read_resp 读出来的FTB项
    • 接口类型:FTBEntry
  • read_hits 命中了哪一路 (way)
    • 接口类型:Valid(UInt(log2Ceil(numWays).W))

其中 req_pc 的接口是 Decoupled 的,也就是说含有 valid 和 ready 信号。 FTB 需要在 s1 阶段开始之前便获取到 pc,因此 s0_pc 被送入 req_pc 接口,s0_fire 信号被连接至 req_pc 的 valid 信号,ready 信号被连接至预测器的流水控制信号 s1_ready

s0_fire 之后进入 s1 阶段,s0_fire 的下一个周期,也就是在 s1_fire 的同时,FTBBank 已经将读出的 FTB 项输出至 read_resp 接口,并计算好了 read_hits 。但此时因为读出已经浪费了过多时延,无法在 s1 阶段进行输出了,因此该读出结果被保存在内部寄存器中。在 s2 和 s3 阶段会在寄存器中读出该 FTB 项并生成预测结果。

FTBBank

FTBBank 中定义了一个存储器来存储所有的 FTB 项,存储器采用组相联的结构(如果你不清楚组相联的含义,请自行 RTFM),共含 512 个组(Set),每组有 4 路(Way),最多能存储 2048 个 FTB 项,存储 FTB 的同时还会存储与 FTB 项对应的 tag 用于匹配 FTB 项。

具体地,tag 被定义为 pc[29:10],即在 pc 中截取了 20 位用于标识 FTB 项,pc 的具体划分方式如下:

  pc: | ... |<-- tag(20 bits) -->|<-- idx(9 bits) -->|<-- instOffset(1 bit) -->|

读出时,向存储器提供组序号(idx),读出该组中所有路,然后检测是否有某一路的 tag 与当前 tag 匹配,如果匹配,则表示命中,读出的 FTB 项通过 read_resp 接口送出,并把命中的路编号通过 read_hits 接口送出。

预测结果生成

正如我们之前提到的,对于 FTB 预测器,它需要向 s2, s3 通道提供由 FTB 项转化来的基础预测结果,FTB 项已经在 s1 阶段被读出并保存。在 s2 和 s3 阶段只需直接读出便生成即可,但需要注意的一点就是保护 TAGE-SC 在 s2 及 s3 通道中产生的预测结果 br_taken_mask 字段,这一字段为条件分支指令提供了精确的预测结果。对于 s1 通道,FTB 预测器不予更改。

s2 和 s3预测结果中的信号产生方式可参考以下列表:

  • hit FTB表项是否命中

    • 生成方式:FTBBank 输出的 read_hits 信号 valid 位有效。
  • slot_valids slot有效位,表示 ftb 项中的每个 slot 是否有效

  • targets 每个slot对应的跳转目标地址

  • offsets 每个slot中的指令相对于预测块起始地址的偏移

  • is_jal 预测块内包含jal指令

  • is_jalr 预测块内包含jalr指令

  • is_call 预测块内包含call指令

  • is_ret 预测块内包含ret指令

  • last_may_be_rvi_call 预测结果预测块末尾可能为一个RVI类型的call指令信号

  • is_br_sharing 最后一个slot (tailSlot) 中存储的是条件分支指令信号

    • 生成方式: FTB 项中对应字段导出
  • fallThroughErr FTB项中记录的 pftAddr 有误

    • 生成方式:比较 pftAddr 代表的预测块结束地址是否大于预测块的起始地址,如果小于,则代表出现错误,此信号置为有效。这种情况可能会发生在 pc 索引到错误的 FTB 项的情况。
  • fallThroughAddr 预测块的结束地址

    • 生成方式:如果 fallThroughErr 无效,则根据 pftAddr 生成,否则将其设置为起始地址 + 预测宽度。
  • br_taken_mask 分支预测结果,每个分支(slot)对应一位,表示该分支是否被预测为 taken

    • 生成方式:若 FTB 项命中并且 always_taken 字段有效,则预测为 taken;否则由相应通道输入接口中提供的 br_taken_mask 信号来决定。
  • jalr_target 本预测块中的 jalr 的跳转目标

    • 生成方式:FTB 项中 tailSlot 中的跳转目标。

FTB meta

在预测的第三周期,FTB 预测器会将本次预测的一些辅助信息输出至 last_stage_meta 中,还会将读出的 FTB 项送到 last_stage_ftrb_entry 接口中。

FTB meta 中含有 hitwriteWay 两个信息,分别表示本次预测是否命中,以及在哪一路读出。之后更新通道中会产生本次预测的更新信息,这两个信息也会随之送入,来指导更新后的 FTB 项写入的位置。

FTB 更新

update 通道中已经为我们指定好了 pc 以及新 FTB 项,并且还有 meta 信息中的 hitwriteWay 。如果 meta 中的 hit 有效,说明之前这一 pc 对应的 FTB 项在存储器中是有存储的,我们只需将它写入到 writeWay 对应的路中即可。

如果无效,说明以前没有存储,但现在是否存储我们并不知道,有可能在此更新请求之前,该 pc 对应的 FTB 项被另一个更新请求写入了。因此我们还需要给 FTBBank 发送一个读请求,来判断其中是否存在对应的 FTB 项。如果存在,下周期可直接写入到该位置,否则,通知 FTBBank 为其重新分配一个位置。

因此 FTB 项的更新需要的周期数需随 hit 情况而定。

我们首先来看一下 FTBBank是怎样处理更新的。

FTBBank 更新

FTBBank 的更新接口如下,分为更新读接口和更新写接口两部分。

  • u_req_pc 更新读请求 pc

    • Flipped(DecoupledIO(UInt(VAddrBits.W)))
  • update_hits 读出的命中情况

    • Valid(UInt(log2Ceil(numWays).W))
  • update_access 有更新请求但 meta 信息指示未命中

    • Bool()
  • update_pc 更新写请求 pc

    • UInt(VAddrBits.W))
  • update_write_data 更新请求写入的数据,valid 有效的时候写

    • Flipped(Valid(new FTBEntryWithTag))
  • update_write_way 更新请求写入的路索引

    • UInt(log2Ceil(numWays).W))
  • update_write_alloc 是否需要分配FTB项(以前未命中)

    • Bool()

对于更新读接口,FTBBank 通过 u_req_pc 信号获取更新读请求,该请求要比预测时的读请求优先级更高,在下一个周期,FTBBank 会将命中情况通过 update_hits 接口送出。update_access 仅用于 FTBBank 内部某些状态判断。

对于更新写接口,FTBBank 通过 update_pc 信号获取更新写请求的 pc,并在 update_write_data 有效时向 update_write_way 对应的位置写入其中的数据。如果 update_write_alloc 有效,说明不能直接写入请求中指定的位置,而是需要重新分配一个位置。

分配的策略如下:

  • 如果所有路均已填充,则使用伪 LRU 替换算法选取要替换的路
  • 若存在空路,否则选取空路。

更新请求时序

  • meta hit 有效 若更新请求中 meta 指示为 hit,则我们直接根据更新请求中的信息,指定好写入的地址和数据即可,写入仅需一周期
  • meta hit 无效 此时,在接收到更新请求后,我们将请求中的 pc 连接至 FTBBank 的读端口,读端口在下一周期将结果返回。由于时延问题,我们将这一结果保存,并在下一周期使用。下一周期根据结果中的命中情况指定是否需要设置 update_write_alloc,并发出写请求。这时的更新过程总共花费了三个周期。

接口列表

信号类型 信号位 信号名 信号描述
input clock 输入时钟
input reset 复位信号
input [35:0] io_reset_vector 用于reset时,reset s1_pc_dup_0 提供的值
input [40:0] io_in_bits_s0_pc_0 输入位s0_pc 的 第0个复制
input [40:0] io_in_bits_s0_pc_1 同上 第1个
input [40:0] io_in_bits_s0_pc_2 同上 第2个
input [40:0] io_in_bits_s0_pc_3 同上 第3个
input io_in_bits_resp_in_0_s2_full_pred_0_br_taken_mask_0 预测结果输入
input io_in_bits_resp_in_0_s2_full_pred_0_br_taken_mask_1
input io_in_bits_resp_in_0_s2_full_pred_1_br_taken_mask_0
input io_in_bits_resp_in_0_s2_full_pred_1_br_taken_mask_1
input io_in_bits_resp_in_0_s2_full_pred_2_br_taken_mask_0
input io_in_bits_resp_in_0_s2_full_pred_2_br_taken_mask_1
input io_in_bits_resp_in_0_s2_full_pred_3_br_taken_mask_0
input io_in_bits_resp_in_0_s2_full_pred_3_br_taken_mask_1
input io_in_bits_resp_in_0_s3_full_pred_0_br_taken_mask_0
input io_in_bits_resp_in_0_s3_full_pred_0_br_taken_mask_1
input io_in_bits_resp_in_0_s3_full_pred_1_br_taken_mask_0
input io_in_bits_resp_in_0_s3_full_pred_1_br_taken_mask_1
input io_in_bits_resp_in_0_s3_full_pred_2_br_taken_mask_0
input io_in_bits_resp_in_0_s3_full_pred_2_br_taken_mask_1
input io_in_bits_resp_in_0_s3_full_pred_3_br_taken_mask_0
input io_in_bits_resp_in_0_s3_full_pred_3_br_taken_mask_1
output io_out_s2_full_pred_0_br_taken_mask_0 s2 阶段输出的完整预测结果
output io_out_s2_full_pred_0_br_taken_mask_1
output io_out_s2_full_pred_0_slot_valids_0
output io_out_s2_full_pred_0_slot_valids_1
output [40:0] io_out_s2_full_pred_0_targets_0
output [40:0] io_out_s2_full_pred_0_targets_1
output [40:0] io_out_s2_full_pred_0_jalr_target
output [3:0] io_out_s2_full_pred_0_offsets_0
output [3:0] io_out_s2_full_pred_0_offsets_1
output [40:0] io_out_s2_full_pred_0_fallThroughAddr
output io_out_s2_full_pred_0_is_br_sharing
output io_out_s2_full_pred_0_hit
output io_out_s2_full_pred_1_br_taken_mask_0 同上
output io_out_s2_full_pred_1_br_taken_mask_1
output io_out_s2_full_pred_1_slot_valids_0
output io_out_s2_full_pred_1_slot_valids_1
output [40:0] io_out_s2_full_pred_1_targets_0
output [40:0] io_out_s2_full_pred_1_targets_1
output [40:0] io_out_s2_full_pred_1_jalr_target
output [3:0] io_out_s2_full_pred_1_offsets_0
output [3:0] io_out_s2_full_pred_1_offsets_1
output [40:0] io_out_s2_full_pred_1_fallThroughAddr
output io_out_s2_full_pred_1_is_br_sharing
output io_out_s2_full_pred_1_hit
output io_out_s2_full_pred_2_br_taken_mask_0 同上
output io_out_s2_full_pred_2_br_taken_mask_1
output io_out_s2_full_pred_2_slot_valids_0
output io_out_s2_full_pred_2_slot_valids_1
output [40:0] io_out_s2_full_pred_2_targets_0
output [40:0] io_out_s2_full_pred_2_targets_1
output [40:0] io_out_s2_full_pred_2_jalr_target
output [3:0] io_out_s2_full_pred_2_offsets_0
output [3:0] io_out_s2_full_pred_2_offsets_1
output [40:0] io_out_s2_full_pred_2_fallThroughAddr
output io_out_s2_full_pred_2_is_jalr
output io_out_s2_full_pred_2_is_call
output io_out_s2_full_pred_2_is_ret
output io_out_s2_full_pred_2_last_may_be_rvi_call
output io_out_s2_full_pred_2_is_br_sharing
output io_out_s2_full_pred_2_hit
output io_out_s2_full_pred_3_br_taken_mask_0 同上
output io_out_s2_full_pred_3_br_taken_mask_1
output io_out_s2_full_pred_3_slot_valids_0
output io_out_s2_full_pred_3_slot_valids_1
output [40:0] io_out_s2_full_pred_3_targets_0
output [40:0] io_out_s2_full_pred_3_targets_1
output [40:0] io_out_s2_full_pred_3_jalr_target
output [3:0] io_out_s2_full_pred_3_offsets_0
output [3:0] io_out_s2_full_pred_3_offsets_1
output [40:0] io_out_s2_full_pred_3_fallThroughAddr
output io_out_s2_full_pred_3_fallThroughErr
output io_out_s2_full_pred_3_is_br_sharing
output io_out_s2_full_pred_3_hit
output io_out_s3_full_pred_0_br_taken_mask_0 s3 阶段输出的完整预测结果
output io_out_s3_full_pred_0_br_taken_mask_1
output io_out_s3_full_pred_0_slot_valids_0
output io_out_s3_full_pred_0_slot_valids_1
output [40:0] io_out_s3_full_pred_0_targets_0
output [40:0] io_out_s3_full_pred_0_targets_1
output [40:0] io_out_s3_full_pred_0_jalr_target
output [40:0] io_out_s3_full_pred_0_fallThroughAddr
output io_out_s3_full_pred_0_fallThroughErr
output io_out_s3_full_pred_0_is_br_sharing
output io_out_s3_full_pred_0_hit
output io_out_s3_full_pred_1_br_taken_mask_0 同上
output io_out_s3_full_pred_1_br_taken_mask_1
output io_out_s3_full_pred_1_slot_valids_0
output io_out_s3_full_pred_1_slot_valids_1
output [40:0] io_out_s3_full_pred_1_targets_0
output [40:0] io_out_s3_full_pred_1_targets_1
output [40:0] io_out_s3_full_pred_1_jalr_target
output [40:0] io_out_s3_full_pred_1_fallThroughAddr
output io_out_s3_full_pred_1_fallThroughErr
output io_out_s3_full_pred_1_is_br_sharing
output io_out_s3_full_pred_1_hit
output io_out_s3_full_pred_2_br_taken_mask_0 同上
output io_out_s3_full_pred_2_br_taken_mask_1
output io_out_s3_full_pred_2_slot_valids_0
output io_out_s3_full_pred_2_slot_valids_1
output [40:0] io_out_s3_full_pred_2_targets_0
output [40:0] io_out_s3_full_pred_2_targets_1
output [40:0] io_out_s3_full_pred_2_jalr_target
output [40:0] io_out_s3_full_pred_2_fallThroughAddr
output io_out_s3_full_pred_2_fallThroughErr
output io_out_s3_full_pred_2_is_jalr
output io_out_s3_full_pred_2_is_call
output io_out_s3_full_pred_2_is_ret
output io_out_s3_full_pred_2_is_br_sharing
output io_out_s3_full_pred_2_hit
output io_out_s3_full_pred_3_br_taken_mask_0 同上
output io_out_s3_full_pred_3_br_taken_mask_1
output io_out_s3_full_pred_3_slot_valids_0
output io_out_s3_full_pred_3_slot_valids_1
output [40:0] io_out_s3_full_pred_3_targets_0
output [40:0] io_out_s3_full_pred_3_targets_1
output [40:0] io_out_s3_full_pred_3_jalr_target
output [3:0] io_out_s3_full_pred_3_offsets_0
output [3:0] io_out_s3_full_pred_3_offsets_1
output [40:0] io_out_s3_full_pred_3_fallThroughAddr
output io_out_s3_full_pred_3_fallThroughErr
output io_out_s3_full_pred_3_is_br_sharing
output io_out_s3_full_pred_3_hit
output [222:0] io_out_last_stage_meta 最后一个阶段输出的 meta 信息
output io_out_last_stage_ftb_entry_valid 最后一个阶段输出的 FTB 项
output [3:0] io_out_last_stage_ftb_entry_brSlots_0_offset
output [11:0] io_out_last_stage_ftb_entry_brSlots_0_lower
output [1:0] io_out_last_stage_ftb_entry_brSlots_0_tarStat
output io_out_last_stage_ftb_entry_brSlots_0_sharing
output io_out_last_stage_ftb_entry_brSlots_0_valid
output [3:0] io_out_last_stage_ftb_entry_tailSlot_offset
output [19:0] io_out_last_stage_ftb_entry_tailSlot_lower
output [1:0] io_out_last_stage_ftb_entry_tailSlot_tarStat
output io_out_last_stage_ftb_entry_tailSlot_sharing
output io_out_last_stage_ftb_entry_tailSlot_valid
output [3:0] io_out_last_stage_ftb_entry_pftAddr
output io_out_last_stage_ftb_entry_carry
output io_out_last_stage_ftb_entry_isCall
output io_out_last_stage_ftb_entry_isRet
output io_out_last_stage_ftb_entry_isJalr
output io_out_last_stage_ftb_entry_last_may_be_rvi_call
output io_out_last_stage_ftb_entry_always_taken_0
output io_out_last_stage_ftb_entry_always_taken_1
input io_ctrl_btb_enable 使能信号
input io_s0_fire_0 s0 阶段流水线控制信号
input io_s0_fire_1
input io_s0_fire_2
input io_s0_fire_3
output io_s1_ready s1 阶段流水线控制信号
input io_s1_fire_0
input io_s1_fire_1
input io_s1_fire_2
input io_s1_fire_3
input io_s2_fire_0 s2 阶段流水线控制信号
input io_s2_fire_1
input io_s2_fire_2
input io_s2_fire_3
input io_update_valid 更新有效性
input [40:0] io_update_bits_pc 传回的预测块pc(用于指示更新的预测块)
input io_update_bits_ftb_entry_valid 是否启用
input [3:0] io_update_bits_ftb_entry_brSlots_0_offset solt 0 中分支指令相对于地址块起始pc的偏移
input [11:0] io_update_bits_ftb_entry_brSlots_0_lower 跳转目标地址的低位
input [1:0] io_update_bits_ftb_entry_brSlots_0_tarStat 跳转后的 pc 高位是否进退位
input io_update_bits_ftb_entry_brSlots_0_sharing 无条件跳转指令槽中存储条件分支指令
input io_update_bits_ftb_entry_brSlots_0_valid 是否启用
input [3:0] io_update_bits_ftb_entry_tailSlot_offset solt 1 中分支指令相对于地址块起始pc的偏移
input [19:0] io_update_bits_ftb_entry_tailSlot_lower 跳转目标地址的低位
input [1:0] io_update_bits_ftb_entry_tailSlot_tarStat 跳转后的 pc 高位是否进退位
input io_update_bits_ftb_entry_tailSlot_sharing 无条件跳转指令槽中存储条件分支指令
input io_update_bits_ftb_entry_tailSlot_valid 是否启用
input [3:0] io_update_bits_ftb_entry_pftAddr Partial Fallthrough Addr 如果预测块中没有跳转,那么程序将会顺序执行到达的地址,预测块的结束地址。
input io_update_bits_ftb_entry_carry pc+pft时是否产生进位
input io_update_bits_ftb_entry_isCall 是否是函数调用
input io_update_bits_ftb_entry_isRet 是否是函数返回
input io_update_bits_ftb_entry_isJalr 是否是 jalr 指令
input io_update_bits_ftb_entry_last_may_be_rvi_call 最后一个指令槽存储的可能是 rvi 的 call 指令
input io_update_bits_ftb_entry_always_taken_0 是否预测为总是跳转
input io_update_bits_ftb_entry_always_taken_1 是否预测为总是跳转
input io_update_bits_old_entry 是否是旧的 FTB 项
input [222:0] io_update_bits_meta meta 信息

3.5 - ITTAGE 分支预测器

功能介绍

对于一般条件分支指令,只需要预测跳转(taken),或者不跳转(no taken),但是对于间接跳转,如call/jump等指令,需要预测跳转到哪里去(Target)。为了让TAGE支持预测跳转地址,ITTAGE(Indirect Target TAGE)应运而生。

ITTAGE 与 TAGE 的主要区别在于,在T0和Tn表中,多了 Target PC 数据。预测时,ITTAGE选择匹配到的、历史最长的表项中的 Target 作为预测结果,并利用 2bit 饱和计数器决定是否输出该结果,或选用替代预测结果。TAGE 预测器说明,请参考 TAGE-SC分支预测器

昆明湖 ITTAGE 分支预测器

由于在昆明湖的BPU设计中,采用多预测器级联的方式进行预测,因此在子预测器实现上,会与原始预测器有所不同,其中多数表现在默认预测结果上。

基本功能

ITTAGE基本功能类似 TAGE 分支预测器,但存在以下不同点:

  1. 在表项中增加了 Target 作为跳转的目标地址项,用于预测跳转目标地址。
  2. 饱和计数器ctr也不再提供预测方向,而改为决定是否输出结果(只是预测信息)。
  3. 由于每个分支预测块中只有一个间接跳转指令,所以ITTAGE也只考虑一条指令。

流水线

ITTAGE 内含三个流水级,第一级计算索引,第二级通过索引读出SRAM表中的结果

  1. 第0拍,s0:第一个流水级的输入,一般是pc和折叠历史。

第一流水级运行:计算index。通过寄存器输出到s1

  1. 第1拍,s1:第二个流水级的输入,为第一级阶段计算好的index等数据。

第二流水级运行:访存SRAM,读取预测用信息。通过寄存器输出到s2

  1. 第2拍,s2:第三个流水级的输入,为第二级从SRAM中读出的原始预测信息

**第三流水级运行:**处理原始预测信息,决定是否输出预测结果。

  1. 第3拍,s3:预测结果就绪,此时已经可以使用预测结果

数据结构

在昆明湖的实现中,T0与Tn的表结构如下所示:

预测器 作用 表项构成 项数
基准预测器T0 用于在其他预测器的预测结果都无效时输出预测结果 虚表,不存在。 直接将上级预测器FTB 的预测结果作为表项结果 虚表,不存在。 直接将上级预测器FTB结果作为索引到的结果
预测表T1-T2 对每个预测块的输入,所有Tn表都进行预测,在所有预测有效的结果中,选择历史记录最长的结果作为 原始预测信息。历史记录长度由输入的H决定 target: 41 bits; valid: 1bit; tag: 9bits; ctr: 2bits; us: 1bit(usefulness计数器) 256项
预测表T3-T5 512项

T0,Tn表的检索方法

检索方法与 TAGE 分支预测器一致,只是各表的配置选项不同。

表名称 FH长度 FH1长度 FH2长度 最近历史长度(用到GH中的位数)
T1 4比特 4比特 4比特 低4位,即把最新4位历史,折叠成FH、FH1、FH2
T2 8比特 8比特 8比特 低8位,即把最新8位历史,折叠成FH、FH1、FH2
T3 9比特 9比特 8比特 低13位,即把最新13位历史,折叠成FH、FH1、FH2
T4 9比特 9比特 8比特 低16位,即把最新16位历史,折叠成FH、FH1、FH2
T5 9比特 9比特 8比特 低32位,即把最新32位历史,折叠成FH、FH1、FH2

其他过程(计算方式计算公式)类似 TAGE-SC分支预测器

替代预测器

当Tn表给出的预测结果,“预测信心”不足时,需要对预测结果进行跳转,成为“替代预测器”。该过程与 TAGE 类似,具体请先阅读TAGE的对应部分。与 TAGE 不同,ITTAGE 的 ctr 并不给出预测方向,只决定是否输出该结果(预测信心)。当 ctr 为 2b00 的时候,即认定为弱信心。选择替代预测结果:

  1. 如果命中多个个表,输出长度第二长的表中表项的 Target
  2. 否则输出 T0 Target(FTB Target)

预测过程

预测过程与 TAGE 类似,但与 TAGE 不同的是ITTAGE 多了一步根据 ctr 决定是否输出预测结果。具体过程如下:

  1. 当 ITTAGE 表项的 ctr 不为 2b00 时,输出 Target
  2. 当 ITTAGE 表项的 ctr 为 2b00 时,输出替代预测结果
    1. 如果有次长的历史(第二个表也命中),则输出 次长的 Target
    2. 否则输出 FTB Target
  3. 当 ITTAGE 表项没命中时,输出 T0 Target(FTB Target)

训练过程

该过程与 TAGE 类似,具体不同点如下:

  1. 表项更新(原始预测数据):
    1. ctr:
      1. 若预测地址与实际一致,则将对应provider(提供原始预测数据的表)表项的ctr计数器自增1
      2. 若预测地址与实际不一致,则将对应provider表项的ctr计数器自减1
      3. ITTAGE中,会根据ctr判断是否采取这个预测的跳转目标结果。 如果多个表均命中,且历史最长表的ctr为0,则采取替代预测逻辑(历史次长的表 或 T0)。 更新时始终更新历史最长的表,如果采用了替代预测,也同时更新替代预测表。
    2. target:
      1. 当待更新表项在进行本次预测时的ctr为0时,直接将实际的最终跳转结果存入target,覆盖
      2. 如果是在申请新表项,直接将实际的最终跳转结果存入target
      3. 否则不修改target
    3. usefulness:
      1. 当provider预测正确替代预测错误时provider的usefulness置1
      2. 如果替代预测是弱信心,并且预测正确,则provider的usefulness置1。 如果替代预测是弱信心,并且预测错误,则provider的usefulness置0。
    4. 新表项:
      1. 每次由于历史最长表有信心的预测出错时(不是由于使用替代预测出错),会尝试随机在历史更长表中申请一个表项。能申请到的条件是对应项的useful为0
      2. 如果更长的useful均不为0,则分配失败
  2. 重置useful位:
    1. 每次预测出错时会申请新表项,如果分配失败则tickCtr(用于重置全部useful的8bit饱和计数器) +1,成功则 -1
    2. 当tickCtr为最大值时,设置ITTAGE中的所有useful为0,并设置tickCtr为0。

接口列表

接口类型 位宽 信号名 备注
input clock
input reset
input [40:0] io_in_bits_s0_pc_3 用于预测的PC
input [7:0] io_in_bits_folded_hist_3_hist_14_folded_hist T2 折叠历史
input [8:0] io_in_bits_folded_hist_3_hist_13_folded_hist T3 折叠历史
input [3:0] io_in_bits_folded_hist_3_hist_12_folded_hist T1 折叠历史
input [8:0] io_in_bits_folded_hist_3_hist_10_folded_hist T5 折叠历史
input [8:0] io_in_bits_folded_hist_3_hist_6_folded_hist T4 折叠历史
input [7:0] io_in_bits_folded_hist_3_hist_4_folded_hist T3 折叠历史
input [7:0] io_in_bits_folded_hist_3_hist_3_folded_hist T5 折叠历史
input [7:0] io_in_bits_folded_hist_3_hist_2_folded_hist T4 折叠历史
input [40:0] io_in_bits_resp_in_0_s2_full_pred_3_jalr_target FTB项输入,提供替代预测
output [40:0] io_out_s3_full_pred_0_jalr_target
output [40:0] io_out_s3_full_pred_1_jalr_target
output [40:0] io_out_s3_full_pred_2_jalr_target
output [40:0] io_out_s3_full_pred_3_jalr_target
output [222:0] io_out_last_stage_meta [100:0] 有效,是ITTAGE的Meta信息
input io_s0_fire_3 s0阶段使能信号
input io_s1_fire_3 s1阶段使能信号
input io_s2_fire_0 s2阶段使能信号,相同
input io_s2_fire_1
input io_s2_fire_2
input io_s2_fire_3
input io_update_valid 是否进行更新
input [40:0] io_update_bits_pc 待更新的预测块pc索引
input [7:0] io_update_bits_spec_info_folded_hist_hist_14_folded_hist T2 更新时传入的历史
input [8:0] io_update_bits_spec_info_folded_hist_hist_13_folded_hist T3 更新时传入的历史
input [3:0] io_update_bits_spec_info_folded_hist_hist_12_folded_hist T1 更新时传入的历史
input [8:0] io_update_bits_spec_info_folded_hist_hist_10_folded_hist T5 更新时传入的历史
input [8:0] io_update_bits_spec_info_folded_hist_hist_6_folded_hist T4 更新时传入的历史
input [7:0] io_update_bits_spec_info_folded_hist_hist_4_folded_hist T3 更新时传入的历史
input [7:0] io_update_bits_spec_info_folded_hist_hist_3_folded_hist T5 更新时传入的历史
input [7:0] io_update_bits_spec_info_folded_hist_hist_2_folded_hist T4 更新时传入的历史
input [3:0] io_update_bits_ftb_entry_tailSlot_offset 待更新的FTB项offset
input io_update_bits_ftb_entry_tailSlot_sharing 待更新的FTB项是否是有条件跳转
input io_update_bits_ftb_entry_tailSlot_valid 待更新的tailSlot是否启用
input io_update_bits_ftb_entry_isRet tailSlot是否是Ret指令
input io_update_bits_ftb_entry_isJalr tailSlot是否是Jalr指令
input io_update_bits_cfi_idx_valid 控制流指令在预测块中的索引.valid信号
input [3:0] io_update_bits_cfi_idx_bits 控制流指令在预测块中的索引
input io_update_bits_jmp_taken 预测块内无条件跳转指令被触发
input io_update_bits_mispred_mask_2 是否预测错误
input [222:0] io_update_bits_meta 预测时传出 meta 信息的[222:25] 即{25h0, _ubtb_io_out_last_stage_meta[5:0] ,_tage_io_out_last_stage_meta[87:0] ,_ftb_io_out_last_stage_meta[2:0], _ittage_io_out_last_stage_meta[100:0]}
input [40:0] io_update_bits_full_target 预测块的跳转目标(下一个预测块的起始地址)

不产生影响的透传信号

这里的信号不产生影响,不重要
接口类型 位宽 信号名 备注
input io_in_bits_resp_in_0_s2_full_pred_0_br_taken_mask_0 从FTB输入 完全透传到输出 包括jalr_target
input io_in_bits_resp_in_0_s2_full_pred_0_br_taken_mask_1
input io_in_bits_resp_in_0_s2_full_pred_0_slot_valids_0
input io_in_bits_resp_in_0_s2_full_pred_0_slot_valids_1
input [40:0] io_in_bits_resp_in_0_s2_full_pred_0_targets_0
input [40:0] io_in_bits_resp_in_0_s2_full_pred_0_targets_1
input [40:0] io_in_bits_resp_in_0_s2_full_pred_0_jalr_target
input [3:0] io_in_bits_resp_in_0_s2_full_pred_0_offsets_0
input [3:0] io_in_bits_resp_in_0_s2_full_pred_0_offsets_1
input [40:0] io_in_bits_resp_in_0_s2_full_pred_0_fallThroughAddr
input io_in_bits_resp_in_0_s2_full_pred_0_is_br_sharing
input io_in_bits_resp_in_0_s2_full_pred_0_hit
input io_in_bits_resp_in_0_s2_full_pred_1_br_taken_mask_0
input io_in_bits_resp_in_0_s2_full_pred_1_br_taken_mask_1
input io_in_bits_resp_in_0_s2_full_pred_1_slot_valids_0
input io_in_bits_resp_in_0_s2_full_pred_1_slot_valids_1
input [40:0] io_in_bits_resp_in_0_s2_full_pred_1_targets_0
input [40:0] io_in_bits_resp_in_0_s2_full_pred_1_targets_1
input [40:0] io_in_bits_resp_in_0_s2_full_pred_1_jalr_target
input [3:0] io_in_bits_resp_in_0_s2_full_pred_1_offsets_0
input [3:0] io_in_bits_resp_in_0_s2_full_pred_1_offsets_1
input [40:0] io_in_bits_resp_in_0_s2_full_pred_1_fallThroughAddr
input io_in_bits_resp_in_0_s2_full_pred_1_is_br_sharing
input io_in_bits_resp_in_0_s2_full_pred_1_hit
input io_in_bits_resp_in_0_s2_full_pred_2_br_taken_mask_0
input io_in_bits_resp_in_0_s2_full_pred_2_br_taken_mask_1
input io_in_bits_resp_in_0_s2_full_pred_2_slot_valids_0
input io_in_bits_resp_in_0_s2_full_pred_2_slot_valids_1
input [40:0] io_in_bits_resp_in_0_s2_full_pred_2_targets_0
input [40:0] io_in_bits_resp_in_0_s2_full_pred_2_targets_1
input [40:0] io_in_bits_resp_in_0_s2_full_pred_2_jalr_target
input [3:0] io_in_bits_resp_in_0_s2_full_pred_2_offsets_0
input [3:0] io_in_bits_resp_in_0_s2_full_pred_2_offsets_1
input [40:0] io_in_bits_resp_in_0_s2_full_pred_2_fallThroughAddr
input io_in_bits_resp_in_0_s2_full_pred_2_is_jalr RAS 模块使用的信息,透传
input io_in_bits_resp_in_0_s2_full_pred_2_is_call
input io_in_bits_resp_in_0_s2_full_pred_2_is_ret
input io_in_bits_resp_in_0_s2_full_pred_2_last_may_be_rvi_call
input io_in_bits_resp_in_0_s2_full_pred_2_is_br_sharing 从FTB输入 完全透传到输出 包括jalr_target fallThroughErr 表示 FTB项 中记录的 pftAddr 有误 生成方式:比较 pftAddr 代表的预测块结束地址是否大于预测块的起始地址,如果小于,则代表出现错误,此信号置为有效。这种情况可能会发生在 pc 索引到错误的 FTB 项的情况。 FTQ使用这个变量,与ITTAGE无关
input io_in_bits_resp_in_0_s2_full_pred_2_hit
input io_in_bits_resp_in_0_s2_full_pred_3_br_taken_mask_0
input io_in_bits_resp_in_0_s2_full_pred_3_br_taken_mask_1
input io_in_bits_resp_in_0_s2_full_pred_3_slot_valids_0
input io_in_bits_resp_in_0_s2_full_pred_3_slot_valids_1
input [40:0] io_in_bits_resp_in_0_s2_full_pred_3_targets_0
input [40:0] io_in_bits_resp_in_0_s2_full_pred_3_targets_1
input [3:0] io_in_bits_resp_in_0_s2_full_pred_3_offsets_0
input [3:0] io_in_bits_resp_in_0_s2_full_pred_3_offsets_1
input [40:0] io_in_bits_resp_in_0_s2_full_pred_3_fallThroughAddr
input io_in_bits_resp_in_0_s2_full_pred_3_fallThroughErr
input io_in_bits_resp_in_0_s2_full_pred_3_is_br_sharing
input io_in_bits_resp_in_0_s2_full_pred_3_hit
input io_in_bits_resp_in_0_s3_full_pred_0_br_taken_mask_0 除了 jalr_target 可能被修改,其他都是透传
input io_in_bits_resp_in_0_s3_full_pred_0_br_taken_mask_1
input io_in_bits_resp_in_0_s3_full_pred_0_slot_valids_0
input io_in_bits_resp_in_0_s3_full_pred_0_slot_valids_1
input [40:0] io_in_bits_resp_in_0_s3_full_pred_0_targets_0
input [40:0] io_in_bits_resp_in_0_s3_full_pred_0_targets_1
input [40:0] io_in_bits_resp_in_0_s3_full_pred_0_fallThroughAddr
input io_in_bits_resp_in_0_s3_full_pred_0_fallThroughErr
input io_in_bits_resp_in_0_s3_full_pred_0_is_br_sharing
input io_in_bits_resp_in_0_s3_full_pred_0_hit
input [40:0] io_in_bits_resp_in_0_s3_full_pred_0_jalr_target
input io_in_bits_resp_in_0_s3_full_pred_1_br_taken_mask_0 同上
input io_in_bits_resp_in_0_s3_full_pred_1_br_taken_mask_1
input io_in_bits_resp_in_0_s3_full_pred_1_slot_valids_0
input io_in_bits_resp_in_0_s3_full_pred_1_slot_valids_1
input [40:0] io_in_bits_resp_in_0_s3_full_pred_1_targets_0
input [40:0] io_in_bits_resp_in_0_s3_full_pred_1_targets_1
input [40:0] io_in_bits_resp_in_0_s3_full_pred_1_fallThroughAddr
input io_in_bits_resp_in_0_s3_full_pred_1_fallThroughErr
input io_in_bits_resp_in_0_s3_full_pred_1_is_br_sharing
input io_in_bits_resp_in_0_s3_full_pred_1_hit
input [40:0] io_in_bits_resp_in_0_s3_full_pred_1_jalr_target
input io_in_bits_resp_in_0_s3_full_pred_2_br_taken_mask_0 同上
input io_in_bits_resp_in_0_s3_full_pred_2_br_taken_mask_1
input io_in_bits_resp_in_0_s3_full_pred_2_slot_valids_0
input io_in_bits_resp_in_0_s3_full_pred_2_slot_valids_1
input [40:0] io_in_bits_resp_in_0_s3_full_pred_2_targets_0
input [40:0] io_in_bits_resp_in_0_s3_full_pred_2_targets_1
input [40:0] io_in_bits_resp_in_0_s3_full_pred_2_fallThroughAddr
input io_in_bits_resp_in_0_s3_full_pred_2_fallThroughErr
input io_in_bits_resp_in_0_s3_full_pred_2_is_jalr
input io_in_bits_resp_in_0_s3_full_pred_2_is_call
input io_in_bits_resp_in_0_s3_full_pred_2_is_ret
input io_in_bits_resp_in_0_s3_full_pred_2_is_br_sharing
input io_in_bits_resp_in_0_s3_full_pred_2_hit
input [40:0] io_in_bits_resp_in_0_s3_full_pred_2_jalr_target
input io_in_bits_resp_in_0_s3_full_pred_3_br_taken_mask_0 同上
input io_in_bits_resp_in_0_s3_full_pred_3_br_taken_mask_1
input io_in_bits_resp_in_0_s3_full_pred_3_slot_valids_0
input io_in_bits_resp_in_0_s3_full_pred_3_slot_valids_1
input [40:0] io_in_bits_resp_in_0_s3_full_pred_3_targets_0
input [40:0] io_in_bits_resp_in_0_s3_full_pred_3_targets_1
input [3:0] io_in_bits_resp_in_0_s3_full_pred_3_offsets_0
input [3:0] io_in_bits_resp_in_0_s3_full_pred_3_offsets_1
input [40:0] io_in_bits_resp_in_0_s3_full_pred_3_fallThroughAddr
input io_in_bits_resp_in_0_s3_full_pred_3_fallThroughErr
input io_in_bits_resp_in_0_s3_full_pred_3_is_br_sharing
input io_in_bits_resp_in_0_s3_full_pred_3_hit
input [40:0] io_in_bits_resp_in_0_s3_full_pred_3_jalr_target
input io_in_bits_resp_in_0_last_stage_ftb_entry_valid 透传到output,不做修改 来源是FTB
input [3:0] io_in_bits_resp_in_0_last_stage_ftb_entry_brSlots_0_offset
input [11:0] io_in_bits_resp_in_0_last_stage_ftb_entry_brSlots_0_lower
input [1:0] io_in_bits_resp_in_0_last_stage_ftb_entry_brSlots_0_tarStat
input io_in_bits_resp_in_0_last_stage_ftb_entry_brSlots_0_sharing
input io_in_bits_resp_in_0_last_stage_ftb_entry_brSlots_0_valid
input [3:0] io_in_bits_resp_in_0_last_stage_ftb_entry_tailSlot_offset
input [19:0] io_in_bits_resp_in_0_last_stage_ftb_entry_tailSlot_lower
input [1:0] io_in_bits_resp_in_0_last_stage_ftb_entry_tailSlot_tarStat
input io_in_bits_resp_in_0_last_stage_ftb_entry_tailSlot_sharing
input io_in_bits_resp_in_0_last_stage_ftb_entry_tailSlot_valid
input [3:0] io_in_bits_resp_in_0_last_stage_ftb_entry_pftAddr
input io_in_bits_resp_in_0_last_stage_ftb_entry_carry
input io_in_bits_resp_in_0_last_stage_ftb_entry_isCall
input io_in_bits_resp_in_0_last_stage_ftb_entry_isRet
input io_in_bits_resp_in_0_last_stage_ftb_entry_isJalr
input io_in_bits_resp_in_0_last_stage_ftb_entry_last_may_be_rvi_call
input io_in_bits_resp_in_0_last_stage_ftb_entry_always_taken_0
input io_in_bits_resp_in_0_last_stage_ftb_entry_always_taken_1
output io_out_s2_full_pred_0_br_taken_mask_0 完全透传传入值 prefix: io_in_bits_resp_in_
output io_out_s2_full_pred_0_br_taken_mask_1
output io_out_s2_full_pred_0_slot_valids_0
output io_out_s2_full_pred_0_slot_valids_1
output [40:0] io_out_s2_full_pred_0_targets_0
output [40:0] io_out_s2_full_pred_0_targets_1
output [40:0] io_out_s2_full_pred_0_jalr_target
output [3:0] io_out_s2_full_pred_0_offsets_0
output [3:0] io_out_s2_full_pred_0_offsets_1
output [40:0] io_out_s2_full_pred_0_fallThroughAddr
output io_out_s2_full_pred_0_is_br_sharing
output io_out_s2_full_pred_0_hit
output io_out_s2_full_pred_1_br_taken_mask_0
output io_out_s2_full_pred_1_br_taken_mask_1
output io_out_s2_full_pred_1_slot_valids_0
output io_out_s2_full_pred_1_slot_valids_1
output [40:0] io_out_s2_full_pred_1_targets_0
output [40:0] io_out_s2_full_pred_1_targets_1
output [40:0] io_out_s2_full_pred_1_jalr_target
output [3:0] io_out_s2_full_pred_1_offsets_0
output [3:0] io_out_s2_full_pred_1_offsets_1
output [40:0] io_out_s2_full_pred_1_fallThroughAddr
output io_out_s2_full_pred_1_is_br_sharing
output io_out_s2_full_pred_1_hit
output io_out_s2_full_pred_2_br_taken_mask_0
output io_out_s2_full_pred_2_br_taken_mask_1
output io_out_s2_full_pred_2_slot_valids_0
output io_out_s2_full_pred_2_slot_valids_1
output [40:0] io_out_s2_full_pred_2_targets_0
output [40:0] io_out_s2_full_pred_2_targets_1
output [40:0] io_out_s2_full_pred_2_jalr_target
output [3:0] io_out_s2_full_pred_2_offsets_0
output [3:0] io_out_s2_full_pred_2_offsets_1
output [40:0] io_out_s2_full_pred_2_fallThroughAddr
output io_out_s2_full_pred_2_is_jalr
output io_out_s2_full_pred_2_is_call
output io_out_s2_full_pred_2_is_ret
output io_out_s2_full_pred_2_last_may_be_rvi_call
output io_out_s2_full_pred_2_is_br_sharing
output io_out_s2_full_pred_2_hit
output io_out_s2_full_pred_3_br_taken_mask_0
output io_out_s2_full_pred_3_br_taken_mask_1
output io_out_s2_full_pred_3_slot_valids_0
output io_out_s2_full_pred_3_slot_valids_1
output [40:0] io_out_s2_full_pred_3_targets_0
output [40:0] io_out_s2_full_pred_3_targets_1
output [40:0] io_out_s2_full_pred_3_jalr_target
output [3:0] io_out_s2_full_pred_3_offsets_0
output [3:0] io_out_s2_full_pred_3_offsets_1
output [40:0] io_out_s2_full_pred_3_fallThroughAddr
output io_out_s2_full_pred_3_fallThroughErr
output io_out_s2_full_pred_3_is_br_sharing
output io_out_s2_full_pred_3_hit
output io_out_s3_full_pred_0_br_taken_mask_0 见对应prefix的输入
output io_out_s3_full_pred_0_br_taken_mask_1
output io_out_s3_full_pred_0_slot_valids_0
output io_out_s3_full_pred_0_slot_valids_1
output [40:0] io_out_s3_full_pred_0_targets_0
output [40:0] io_out_s3_full_pred_0_targets_1
output [40:0] io_out_s3_full_pred_0_fallThroughAddr
output io_out_s3_full_pred_0_fallThroughErr
output io_out_s3_full_pred_0_is_br_sharing
output io_out_s3_full_pred_0_hit
output io_out_s3_full_pred_1_br_taken_mask_0 见对应prefix的输入
output io_out_s3_full_pred_1_br_taken_mask_1
output io_out_s3_full_pred_1_slot_valids_0
output io_out_s3_full_pred_1_slot_valids_1
output [40:0] io_out_s3_full_pred_1_targets_0
output [40:0] io_out_s3_full_pred_1_targets_1
output [40:0] io_out_s3_full_pred_1_fallThroughAddr
output io_out_s3_full_pred_1_fallThroughErr
output io_out_s3_full_pred_1_is_br_sharing
output io_out_s3_full_pred_1_hit
output io_out_s3_full_pred_2_br_taken_mask_0 见对应prefix的输入
output io_out_s3_full_pred_2_br_taken_mask_1
output io_out_s3_full_pred_2_slot_valids_0
output io_out_s3_full_pred_2_slot_valids_1
output [40:0] io_out_s3_full_pred_2_targets_0
output [40:0] io_out_s3_full_pred_2_targets_1
output [40:0] io_out_s3_full_pred_2_fallThroughAddr
output io_out_s3_full_pred_2_fallThroughErr
output io_out_s3_full_pred_2_is_jalr
output io_out_s3_full_pred_2_is_call
output io_out_s3_full_pred_2_is_ret
output io_out_s3_full_pred_2_is_br_sharing
output io_out_s3_full_pred_2_hit
output io_out_s3_full_pred_3_br_taken_mask_0 见对应prefix的输入
output io_out_s3_full_pred_3_br_taken_mask_1
output io_out_s3_full_pred_3_slot_valids_0
output io_out_s3_full_pred_3_slot_valids_1
output [40:0] io_out_s3_full_pred_3_targets_0
output [40:0] io_out_s3_full_pred_3_targets_1
output [3:0] io_out_s3_full_pred_3_offsets_0
output [3:0] io_out_s3_full_pred_3_offsets_1
output [40:0] io_out_s3_full_pred_3_fallThroughAddr
output io_out_s3_full_pred_3_fallThroughErr
output io_out_s3_full_pred_3_is_br_sharing
output io_out_s3_full_pred_3_hit
output io_out_last_stage_ftb_entry_valid 完全透传传入的值
output [3:0] io_out_last_stage_ftb_entry_brSlots_0_offset
output [11:0] io_out_last_stage_ftb_entry_brSlots_0_lower
output [1:0] io_out_last_stage_ftb_entry_brSlots_0_tarStat
output io_out_last_stage_ftb_entry_brSlots_0_sharing
output io_out_last_stage_ftb_entry_brSlots_0_valid
output [3:0] io_out_last_stage_ftb_entry_tailSlot_offset
output [19:0] io_out_last_stage_ftb_entry_tailSlot_lower
output [1:0] io_out_last_stage_ftb_entry_tailSlot_tarStat
output io_out_last_stage_ftb_entry_tailSlot_sharing
output io_out_last_stage_ftb_entry_tailSlot_valid
output [3:0] io_out_last_stage_ftb_entry_pftAddr
output io_out_last_stage_ftb_entry_carry
output io_out_last_stage_ftb_entry_isCall
output io_out_last_stage_ftb_entry_isRet
output io_out_last_stage_ftb_entry_isJalr
output io_out_last_stage_ftb_entry_last_may_be_rvi_call
output io_out_last_stage_ftb_entry_always_taken_0
output io_out_last_stage_ftb_entry_always_taken_1

其他Meta见对应子预测其文档

_ubtb_io_out_last_stage_meta

_tage_io_out_last_stage_meta

_ftb_io_out_last_stage_meta

ittage_io_out_last_stage_meta[100:0]

位宽 信号名 备注
100 s3_provided 是否有结果
[99:97] s3_provider 提供结果的表项
96 s3_altProvided 是否有替代预测表项
[95:93] s3_altProvider 提供结果的替代预测表项
92 resp_meta_altDiffers 替代预测的结果是否和主预测的结果不同
91 s3_providerU 主预测的useful bit
[90:89] s3_providerCtr 主预测给出的置信度
[88:87] s3_altProviderCtr 替代预测给出的置信度
86 resp_meta_allocate_valid_r 有空余的表项可供申请
[85:83] resp_meta_allocate_bits_r 申请哪个表中的表项
82 s3_tageTaken_dup_3 在不使用FTB的情况下始为true,使用FTB也为true
[81:41] s3_providerTarget 主预测给出的跳转地址
[40:0] s3_altProviderTarget 替代预测给出的跳转地址

3.6 - RAS 分支预测器

RAS介绍

RAS 指的是 “Return Address Stack”,即返回地址栈。它通过跟踪程序的返回地址,帮助确定程序中的分支行为。由前所述,在程序中存在很多分支:if/else、 switch/case、while/for loop、iteration、call/return

等。RAS分支预测器则专门针对 call/return类型。

function _add(a, b){
    return (a > 0 ? a : 0)  + (b > 0? b : 0);
}

function add(a, b){
    return a + b;
}

function sub(a, b){
    return a - b;
}

function main(){
    a = 1;
    b = 2;
    c = add(a, b);
    d = sub(a, b);
}

如上图所示,在main函数中调用了add和sub,add又调用了函数_add。在该过程中,每次调用(call)的跳转地址和返回的地址固定,且在call的时候,就可以同时得到返回地址。函数的调用过程是一个“栈的出入”过程,因此可以通过“栈”结构进行分支预测:每遇到call指令,把当前PC+4(压缩指令和普通指令的偏移不同)进行压栈push操作;遇到return指令,则进行进行pop操作,得到的地址即为目标跳转地址。在基于“预测块”的BPU中,RAS无法知道当前块是否是call或者ret,因此需要依赖其他预测器,利用前级预测器的结果进行RAS操作。

具体的,在香山的RAS预测器中,其s2阶段,需要判断上一级s2的输出是否预测为call或者ret(即输入信号io.s2_full_pred.hit_taken_on_call/ret有效 ),如果是call则push其后续指令地址入栈,是ret则从栈中pop出地址作为预测结果。因为在BPU的预测器中,人为假定s3阶段得到的结果比s2阶段好,所以RAS预测器在s3阶段需要进行检查,如果上一级的s3预测结果与s2不一致,则采信s3的结果,并按需求判断是否需要撤销或者补齐之前s2阶段的栈操作。例如s2阶段预测为call指令,进行了push操作,而s3为普通分支指令,不需要进行任何操作,此时就需要撤销push;如果s2预测为普通分支指令,s3预测为call,则需要进行push操作补齐。

RAS的栈操作

在普通的RAS设计中,通过栈进行预测函数返回地址。在理想情况下,本节假定RAS可进行随时备份,栈顶指针用sp表示,预测地址用paddr表示。RAS的基本操作有以下4种:

  1. PUSH

由于预测可能会出错,出错时需要回到原始状态,因此在push时需要对当前栈的状态进行备份(在软件领域,通常称为“快照”,本文在后继内容中也用快照进行称呼),标记为s。当遇到call指令时,获取call指令的返回地址 addr = 当前pc + 4(如果是压缩指令则addr = pc+2),然后压栈:sp = addr;sp += 1。

  1. POP

原因同上,对当前栈进行快照,标记为s。当遇到ret指令时,预测的跳转地址 paddr = sp,然后进行出栈, sp = sp - 1。对当前栈进行快照,标记为s。

  1. 重定向操作

由于BPU是对程序的分支进行预测,因此就有“预测对”和“预测错”两种情况,当CPU后端发现分支预测错误,就会进行重定向(redirect)操作,告诉BPU哪个地方预测错误,正确结果是多少。重定向时RAS模块会收到正确的分支和当时预测时RAS栈信息。根据正确分支指令类型不同,首先恢复快照s有如下情形:

(1)之前被预测的指令,实际是 call指令,根据redirect中给的addr地址,执行push操作;

(2)之前被预测的指令,实际是 ret指令,执行pop操作;

  1. 提交操作

提交(Commit)操作即后端告诉前端,之前预测的结果正确。理想情况下,RAS预测器此时不需要进行任何操作。

昆明湖中RAS的实现

由于处理器在执行过程中可能会进行错误的猜测,导致数据污染,RAS需要通过恢复机制来确保数据的准确性。特别是在昆明湖的实际电路设计中,不可能有无限大的栈,也无法随时备份数据。因此,昆明湖的RAS实现中,使用了基于持久化栈的返回地址预测器。具体来说,将RAS分为提交栈(commit stack)和推测栈(spec queue)两个部分,推测栈利用了香山BPU的预测结果来进行预测,然后根据后端返回的信息将数据压入提交栈中。下面详细介绍昆明湖的RAS设计:

每次预测时,RAS栈的快照如何获得?

为了实现对RAS栈进行快照的功能,昆明湖的RAS推测栈采用了基于循环数组的 “链式表示“ 。设计具体如下:

在这个设计中,数据管理是通过一个循环数组来实现的。循环数组有一个起始地址(BOS)和一个尾指针(TOSW),两者之间的数据是有效数据,其他部分为空闲数据。使用链式结构表示“Sepc栈”,每个栈元素记录其上一个数据的编号,进行栈操作时可以通过该编号获取上一个元素。RAS栈的栈底指针与BOS共用。

如图所示的初始状态S,RAS栈的元素为0、1、4、5。元素5记录了其上一个元素的位置4,元素4记录了其上一个元素的位置1。在状态S下进行push操作时,RAS栈顶指针TOSR等于TOSW,在新的TOSR位置7存入新元素,并记录其上一个元素的位置5,然后TOSW后移(TOSW = TOSW + 1)。进行pop操作时,RAS栈顶指针TOSR根据栈顶元素保存的索引移动到上一个元素的位置4,但不会修改内存分配指针TOSW,因此Spec栈的元素不一定是连续的。

在栈不溢出的情况下,RAS栈每次通过TOSW在数组上分配新数据,因此在正常的Push/Pop操作中,所有过程状态和中间数据都被保存。当需要恢复到状态S时,只需复位相应的栈指针。因此,在每次RAS预测时,需要将操作前的栈指针(BOS、TOSR、TOSW)保存于预测结果中,以便用于commit栈操作和在重定向时恢复状态。该结构的优点是可以保存完整的过程数据,但频繁的push操作会导致较大的空间资源消耗。

链式RAS浪费存储空间?

当预测结果被确认(commit)后,表示预测是正确的,不会再进行“栈”回滚。也就是说,RAS预测器在收到预测块“P”的commit消息后,不会再收到P块的重定向消息,因此在对P块进行push操作时的快照将不会被用到。

为了优化,RAS栈中的元素被分类处理:未commit的元素存储在链式结构的推测栈中,已commit的元素存储在基于循环数组的提交栈中。这样,原始的RAS栈被拆分成两部分:未commit的部分用链式结构保存,以便需要时可以恢复快照;已commit的部分用普通栈结构保存,因为它们不需要快速恢复。优化后的RAS结构如下所示:

如上图所示,根据普通预测的call/ret和commit提交的call/ret,把原始的RAS栈可以拆分成两个独立的栈,分别称为“预测栈”( spec_stack,链式结构)和“提交栈”(commit_stack,普通结构),由于RASStack对外表现为当拍内就能完成任务,因此在Stack内需要进行数据旁路,对数据进行缓存,由于栈结构发生了变化,具体的Pop/Push操作如下:

1、遇到普通call和ret:

(1)预测块为call正确,在spec_stack上进行push操作,具体为上述链式栈Push过程,并将产生的快照信息送到FTQ存储;

(2)预测块为ret正确,利用spec_stack的栈顶作为预测值,然后进行pop操作,并将产生的快照信息送到FTQ存储,如果spec_stack为空,则使用commit_stack的栈顶元素作为预测结果(不进行pop操作)。

2、Commit操作:

(1)FTQ执行结果为call正确,通过FTQ传回来的快照信息对commit_stack进行push操作,同时更新spec栈的BOS指针,将此次commit的数据及其之前的数据设置为无效数据。

(2)FTQ执行结果为ret正确,通过FTQ传回来的快照信息对commit_stack上进行普通pop操作,注意到在commit pop之前会先对spec栈进行pop操作,所以commit栈弹出来的地址不需要使用。

3、Redirect操作:

(1)从redirect消息中获取之前预测时的栈指针(BOS、TOSR、TOSW,ssp),覆盖当前值指针值完成预测栈状态回滚,若此时为Call指令或者Ret指令,则再对Spec栈进行对应的操作。

(2)该操作不影响提交栈。

输入端,S3的预测结果和S2不一致时该如何处理?

由于昆明湖 BPU 的假定S3的结果比S2好,因此发生不一致时,需要再次对RAS栈进行修复。具体不一致情况和对应的修复操作如下表所示:

S2预测结果 S3预测结果 修复操作
push keep pop
keep pop pop
pop keep push
keep push push

因为SC预测器的特性,S2和S3的操作不存在pop/push或者push/pop情况

其他优化

  1. 每个RAS的spec栈和commit栈的元素都有一个counter计数器,用于节约重复值(递归调用)。例如:当第一次push地址为0xff00,第二次push的也为0xff00时,则只需要把栈顶元素的 counter 进行+1,不需真的进行push操作。

接口说明

在RAS预测器中,核心组件为 RASStack ,其接口说明如下:

接口名称 功能描述 接口名称 功能描述
in.spec_push_valid 预测有Call指令,Spec压栈 in.s2_fire s2信号有效
in.spec_pop_valid 预测有Ret指令,Spec出栈 in.s3_fire s3信号有效
in.spec_push_addr Ret地址 in.s3_cancel s3和s2的预测结果不一样,需要撤销s2的操作
out.spec_pop_addr RAS的栈顶数据 in.s3_meta s3需要的s2时的现场信息
out.ssp commit栈顶指针 in.s3_missed_pop s3判断需要再次进行pop
out.sctr commit栈顶重复元素计数器 in.s3_missed_push s3判断需要再次进行push
out.nsp commit栈顶,会被ssp覆盖 in.s3_pushAddr 需要再次push时的地址
out.TOSR spec栈栈顶指针 in.redirect_valid 需要重定向
out.TOSW spec栈数据分配指针 in.redirect_isCall 真实执行情况是Call
out.BOS spec栈栈底指针 in.redirect_isRet 真实执行情况是Return
in.commit_push_valid FTQ执行结果为Call正确 in.redirect_meta_ssp 之前预测时的现场信息ssp
in.commit_pop_valid FTQ执行结果为Ret正确 in.redirect_meta_sctr 之前预测时的现场信息sctr
in.commit_push_addr 更新信息中的Ret地址 in.redirect_meta_TOSW 之前预测时的现场信息TOSW
in.commit_meta_TOSW 更新信息中的TOSW
in.redirect_meta_TOSR 之前预测时的现场信息TOSR
in.commit_meta_TOSR 更新信息中的TOSR in.redirect_meta_NOS 之前预测时的现场信息NOS
in.commit_meta_ssp 更新信息中的现场信息SSP in.redirect_callAddr 重定向地址
in.commit_meta_sctr 更新信息中的现场信息SCTR

RASStack模块与BasePredictor的接口关系如下:

stack接口 转换过程 描述
s.spec_push_valid io.s2_fire(2) && s2_full_pred.hit_taken_on_call && !io.s3_redirect(2) s2输入有效,且上级预测为call跳转
s.spec_pop_valid io.s2_fire(2) && s2_full_pred.hit_taken_on_ret && !io.s3_redirect(2) s2输入有效,且上级预测为ret跳转
s.spec_push_addr s2_full_pred.fallThroughAddr + Mux(s2_full_pred.last_may_be_rvi_call, 2.U, 0.U) 上级预测器s2预测的fallThroughAddr(即PC+2),判断是否压缩指令是否需要 +2
s.redirect_isCall redirect.bits.level === 0.U && recover_cfi.pd.isCall
s.redirect_isRet redirect.bits.level === 0.U && recover_cfi.pd.isRet
s.redirect_meta_* redirect.bits.cfiUpdate.*
s.commit_push_valid io.update.is_call_taken call指令预测正确
s.commit_push_valid io.update.is_ret_taken ret指令预测正确
s.commit_push_addr update.ftb_entry.getFallThrough(update.pc) + Mux(update.ftb_entry.last_may_be_rvi_call, 2.U, 0.U) 根据是否为压缩指令,进行地址+2或者+0
s.commit_meta_* io.update.bits.meta.asTypeOf(new RASMeta)
io.out.last_stage_spec_info.* s3_meta.* s3_meta = RegEnable(s2_meta, io.s2_fire(2))由s2_meta延迟一怕得到
io.out.last_stage_meta s3_meta
io.out.s2.full_pred.*.jalr_target :=stack.spec_pop_addr 预测地址(栈顶地址,只预测ret)
io.out.s3.full_pred.*.jalr_target :=RegEnable(stack.spec_pop_addr, io.s2_fire(2)) 由s2延迟一拍得到
io.out.s2/3.full_pred.targets.last :=Mux(s2/3_is_jalr, s2/3_jalr_target, s2/3_last_target_in) 如果时call执行,更新targets.last的结果

时序说明

在RAS中,只涉及到2拍,S2和S3。

S2中的主要工作:

1、根据上一个预测器的S2预测结果,完成通过Push/PoP过程完成预测,并得到结果sepc_pop_addr。

2、根据commit信号执行更新操作

S3中的主要工作:

1、根据上一个预测器S3中的预测结果,以及S2进行的操作,判断是否需要进行Pop/Push的撤销操作。预测器假定S3的预测结果比S2好,如果发生S2和S3预测不一致时,RAS预测器采信S3结果进行栈操作。

2、S3和S2的预测过程一致,只是数据不同。

3、执行重定向(重定向信息由前一拍获得)操作

4 - 功能列表

硬件验证中的 功能列表(Feature List) 列出了需要验证的设计的各种功能、特性。本节提供了每个验证任务的基础功能列表,以供参考。

硬件验证中的 功能列表(Feature List) 列出了需要验证的设计的各种功能、特性。在开始验证之前,我们一般都会先列出该设计的功能列表,来明确我们本次任务需要对哪些特性进行验证。

如果要验证功能列表中的一个特征,我们将会针对这一特征构建所需的测试点,并编写测试用例来覆盖这些测试点。因此功能列表不仅仅是对待验证设计的一份功能描述,还是对我们一次验证方向的指引,也是对一次验证的任务量的简单概括。

在本次验证中,我们为每个验证任务(即 BPU 顶层及每个子预测器)提供了一份基础的功能列表,这些特征仅涵盖了对应模块的基础功能,在你的验证中,必须保证本次验证可以覆盖到这些特征。在此基础上,你还可以对列表中没有涵盖到的特征进行补充,以保证最后的交付质量。

在本节文档中,你只需要查看你将要验证模块所对应的文档即可。

4.1 - BPU Top 功能列表

功能列表(Feature List)

  1. 支持 uFTB 子预测器
  2. 支持 TAGE-SC 子预测器
  3. 支持 FTB 子预测器
  4. 支持 ITTAGE 子预测器
  5. 支持 RAS 子预测器
  6. 支持三阶段预测结果及其他信息输出
  7. 支持预测结果重定向信号生成
  8. 支持流水线控制信号生成
  9. 支持 PC 生成
  10. 支持全局分支历史维护
  11. 支持分支折叠历史维护
  12. 支持重定向请求响应,恢复状态
  13. 支持更新请求响应

4.2 - uFTB 功能列表

功能列表(Feature List)

  1. 支持基于 FTB 项的预测
  2. 支持两比特饱和计数器
  3. 支持 s1 通道基础预测结果输出及 meta 信息输出
  4. 支持更新请求响应,更新内部 FTB 及两比特饱和计数器。

4.3 - FTB 功能列表

功能列表(Feature List)

  1. 支持 FTB 项存储
  2. 支持 s2, s3 通道基础预测结果输出以及 meta 信息输出
  3. 支持更新请求响应,更新内部 FTB 项

4.4 - TAGE-SC 功能列表

功能列表(Feature List)

  1. s2 TAGE 输出预测结果
  2. s3 SC 输出预测结果
  3. s2 TAGE 输出meta信息
  4. s3 SC 输出meta信息
  5. TAGE 进行更新训练
  6. 检查申请新表项
  7. 全局重置useful
  8. SC 进行更新训练

4.5 - ITTAGE 功能列表

功能列表(Feature List)

  1. s2 ITTAGE 决定是否产生预测结果
  2. s3 ITTAGE 读取预测的跳转目标
  3. s3 ITTAGE 输出meta信息
  4. ITTAGE 进行更新训练
  5. 检查申请新表项
  6. 全局重置useful
  7. 预测方向

4.6 - RAS 功能列表

功能列表(Feature List)

  1. 支持预测器的开启关闭操作
  2. 支持s3预测结果覆盖s2预测结果
  3. 支持ras栈的push、pop操作
  4. 支持ras栈的redirect操作
  5. 支持ras栈的update操作
  6. 支持base predictor接口
  7. 符合标准RAS预测器预测流程