具体微结构

首先先翻译一下:

在[14]中,我们研究了分支预测器与指令缓存的解耦。为了提供解耦的前端,提取目标队列(FTQ)用于桥接分支预测器和指令高速缓存之间的间隙。每个周期,分支预测器都会产生一个提取目标块预测,并将其存储在FTQ中,最终由指令缓存使用。FTQ提供允许分支预测器和指令高速缓存自主操作所需的缓冲。当分支预测器由于高速缓存未命中或指令缓冲区已满而暂停时,FTQ允许分支预测器在指令高速缓存之前工作。如果指令高速缓存是多端口的,则可以在单个周期内消耗多个有效的FTQ条目,直到端口耗尽。

任何分支预测器(基本块目标缓冲器[22]、提前两个块预测器[15]或提取目标缓冲器[14])都可以放在FTQ之前,并产生要由指令高速缓存消耗的提取块地址。正是FTQ允许分支预测器在指令缓存之前运行。

image-20250507155259388

FTQ包含一个提取块,当数据缓存或者指令缓存未命中,从PIQ开始预取。

Prefetch Buffer 的工作机制

在论文提出的 Fetch Directed Prefetching (FDP) 架构中,Prefetch Buffer 是一个关键组件,用于临时存储通过分支预测器前瞻生成的指令缓存块。以下是其具体工作原理和设计细节的详细解析:


1. Prefetch Buffer 的核心作用

  • 目标:通过提前将未来可能需要的指令缓存块加载到缓冲区,减少指令缓存未命中带来的延迟。
  • 数据来源:由 Fetch Target Queue (FTQ) 中的候选预取地址生成。
  • 与指令缓存的协作:在指令缓存查询时,Prefetch Buffer 并行查找;若命中,则直接将缓存块插入指令缓存。

2. Prefetch Buffer 的结构设计

-

FIFO 队列结构

  • 采用先进先出(FIFO)的队列结构,新预取的缓存块插入队尾,最旧的块在队首。

  • 容量:论文中未明确指定具体条目数,但提及使用类似流式缓冲区的设计(例如 4 条目或更多)。

  • 条目内容:每个条目存储一个完整的指令缓存块(如 32 字节)及其地址标签。

  • 替换策略:当缓冲区满时,新预取请求需等待旧条目被消费或替换。


3. 预取流程的触发与管理

-

预取地址的生成

  • FTQ 中的每个条目包含多个候选预取地址(每个地址对应一个缓存块)。

  • 这些地址通过分支预测器生成,并标记为“候选预取位”(Candidate Bit)。

Prefetch Instruction Queue (PIQ)

  • 候选地址首先进入 PIQ 等待调度。
  • 优先级:指令缓存未命中 > 数据缓存未命中 > 预取请求。
  • 当 L2 总线空闲时,从 PIQ 中取出地址发起预取请求。

4. 预取缓冲区的操作流程

1.

预取填充

  • 从 PIQ 获取地址后,向 L2 缓存发起预取请求。
  • 预取的数据块存入 Prefetch Buffer 队尾。

2.

指令缓存查询时的并行查找

  • 当指令缓存需要访问某个地址时,同时查询指令缓存和 Prefetch Buffer。
  • 若在 Prefetch Buffer 中找到匹配项(命中):
    • 命中条目(通常是最旧条目)被移出缓冲区。
    • 该缓存块直接插入指令缓存,替换原有未命中的块。

3.

分支误预测的处理

  • 发生分支误预测时,FTQ 和 Prefetch Buffer 会被清空(Flush),确保预取路径与正确执行路径一致。

如果cache未命中,且预取缓冲区有数据,此时如何解决?

论文并未提出,不过如果命中可以直接从缓冲区取出数据,类似于压缩队列

image-20250425144739509

该论文首先去给出改进那部分对性能提升最大,首要为LLC,其次就是L1-RF

3. REGISTER FILE PREFETCH (RFP)(核心技术)

执行时的相应加载会检查预测地址是否与加载地址匹配,如果匹配,则预取的数据会被提供给依赖项,加载过程完全绕过缓存。否则,加载会像传统的OOO管道一样进行缓存访问

image-20250425145740214

3.1 设计原则

  • Timeliness(及时性)
    • 选择在重命名阶段后触发预取(而非前端),因:
      • 63%的负载在分配时操作数未就绪,提供预取时间窗口。
      • 已知物理寄存器ID(prfid),可直接写入数据。
  • Bandwidth(带宽)
    • 正确预取时无需二次验证,节省L1带宽;错误预取仅需重新访问L1。
  • Accuracy(准确性)
    • 低置信度预测即可触发(错误代价低),优先服务常规负载。

我们使用负载PC查找PT,以标记符合RFP的负载。当加载被标记为RFP合格时,该加载的预取包被发送到LSQ(加载-存储-队列)和L1高速缓存以进行进一步处理。

预取包包含要预取的虚拟地址和预取数据要写入的加载的物理目的寄存器id (prfid)。请注意,指令的prfid只有在寄存器重命名后才知道。因此,寄存器重命名后会立即触发预取请求。严格地说,prfid可以在RFP管道中稍后提供,因为只有写回时才需要它。PT查找可以在管线中较早发生,在重命名之前,并且我们不期望它在时序关键路径上。

预取包首先与其它负载/RFP请求仲裁对L1端口的访问。当针对常规负载进行投标时,我们给予RFP预取最低的优先级。这可以防止常规负载的延迟升级。RFP队列被组织成一个FIFO,旧的RFP请求比新的获得优先权。预取将从L1获取数据,并将其写入prfid的寄存器文件。

如何处理store?

为了保证正确性,RFP考虑了所有可能修改负载数据的存储。当一个RFP被发送到L1和LSQ时,它会扫描所有较旧的商店(按照先老后小的顺序),并将其地址与商店地址进行匹配。匹配时,它等待存储完成,并使用存储数据而不是缓存数据进行预取。如果存储的地址不可用(意味着存储还没有执行),我们依靠内存歧义消除(MD) [14]机制来决定RFP是需要等待存储还是跳过它

流水线简化

如果一个RFP请求未命中L1,我们允许它继续处理,并从较低级别的缓存中获取数据,类似于常规的按需加载。在DTLB未命中的情况下,我们为了流水线简化而丢弃RFP,因为TLB未命中需要很长的延迟,并且在这种情况下RFP将没有足够的剩余运行时间。

  • 写一个超标量处理器

目前通过官方的测试,但并未进行性能调优。

  • nemu启动linux(低优先级)(7-8月目标)

目前可以启动on mmu linux ,以及opensbi

  • gem5加入data prefetcher,并评估性能(5-6月目标)

  • boom加入data prefetcher,并评估性能(6-7月目标)

  • MEEK改进,并参与流片工作(最高优先级,持续推进)

IPCP算是一个混合架构的预取器

为什么要做这个预取?

每个PC都有自己独特的访存行为

举例:

例如,下面是SPEC CPU 2017基准测试中一个名为bwaves的IPA的L1-D访问模式(根据缓存线对齐地址):C0,C3,C6,C7,C9。

这个IP在大多数情况下遵循三的恒定步幅。在这种情况下,简单的IP跨距预取器可以提供高预取覆盖率。这是来自名为mcf的基准的IPB的另一个访问模式:C0、C1、C3、C4、C6、C7。这个IP的步幅模式是1,2,1,2,1。

在这种情况下,IP-stride预取器提供零覆盖,因为预取器不能达到高置信度

lbm和gcc等流基准中常见的另一种访问模式如下:IPC (C0、C2、C1)、IPD(C3、C6、C4、C5)和IPE(C9、C8、C7)。这是一个Global Stream。观察全局模式,我们可以看到所有的访问都是连续的,并且局限于一个小的内存区域。然而,基于程序顺序,它们的访问模式有点混乱。在这种情况下,许多像IPC、IPD和IPE这样的IP都跟随Global Stream。

显然,IP是独特的,可以根据其访问模式分为不同的类别。请注意,特定IP可以从一种访问模式移动到另一种访问模式,并且可以在一种或多种访问模式下保持活动状态

image-20250425162533460

L1 pretch是对性能影响最大的

IPCP微结构

我们提出了一种空间IPCP,将IP分为三类。我们不跨页面边界进行预取,因为IPCP是一个简单的空间预取器,它在一个小区域(2KB和4KB)2内进行预取。

1. 常量步幅预取器(Constant Stride, CS)

image-20250425163617582

  • 目标场景
    处理仅由控制流驱动的规律性步幅访问。例如,循环中按固定间隔访问数组元素(如stride=3的访问序列C0, C3, C6, C9)。

  • 实现机制

    • IP表(IP Table):记录指令指针(IP)的历史步幅和置信度(2-bit计数器)。

    • 页地址处理:通过last-vpagelast-line-offset检测跨页访问,计算跨页步幅(如从页末尾offset=63到下一页offset=0仍视为stride=1)。

    • 触发条件:当步幅置信度足够高时,预取地址为:

      k为预取深度,默认3)。

  • 优势
    低开销(仅需记录步幅和置信度),适合简单循环或数组遍历。

**Training phase:**IP的置信度达到一定值

Trained phase: prefetch_address=current_address+k×learned_stride ,注意,学习的IP在低置信度的情况下停止预取,并且在获得置信度之后再次开始预取


2. 复杂步幅预取器(Complex Stride, CPLX)

image-20250425163655050

  • 目标场景
    处理控制流与数据流耦合的非固定步幅,例如:
    • 非2的幂次内存布局(如结构体跨缓存行访问,步幅为1,2,1,2)。
    • 嵌套循环导致的步幅变化(外层循环步幅固定,内层步幅动态变化)。
  • 实现机制
    • 复杂步幅模式表(CSPT):记录IP的近期步幅序列(如7个历史步幅),通过签名匹配预测未来步幅。
    • 置信度控制:低置信度时退化为NL(Next-Line)预取。
  • 优势
    覆盖IP-stride无法处理的非规律性但局部可预测的访问模式,如不规则数据结构的遍历。

**Training phase: **每当IP看到相同的步幅时,置信度计数器就加1,否则就减1。此步距与现有签名进行哈希运算,并再次查找CSPT以发出预取请求。根据以下等式,将先前获得的步距添加到签名中:signature = (signature << 1) ˆ stride,请注意,我们将签名移位一位,以便能够适应高度复杂的步幅模式。因此,一个模式可以产生许多签名,但是我们在CSPT中没有观察到太多的碰撞,因为在同一时间点没有许多CPLX IPs

Trained phase: 每当签名指向步距时,并且如果置信度足够高(在我们的例子中≥1),复合步距被添加到高速缓存行以产生预取地址。这种预测一直持续到达到预取程度计数(degree)。如果置信度值为零,则使用上述等式将步距添加到签名,以预测下一步距(3),并且不进行预取

其与SPP的区别:

我们发现,在有些情况下,IP驱动的复杂大步掌握着关键。(I)存储器访问(对于给定的IP)有时不是2的幂(跨高速缓存行的数据结构中的存储器布局),导致非恒定的步幅模式。例如,考虑一个8字节的高速缓存行,如果每第12个字节被访问,则访问产生如下步长:字节地址:0,12,36,48,72;高速缓存行对齐地址:0,1,3,4,6;步幅:1,2,1,2。(ii)另一种情况是通过不同级别的循环进行访问。

外部循环可以进行恒定步距访问(很容易被CS类捕获)。然而,内部循环可以进行不同的步幅访问(取决于外部循环的步幅),从而导致步幅模式中的颠簸。基于IP的CPLX可以利用这种模式。

此外,CPLX类侧重于复杂步幅的局部顺序(捕获控制和数据流),这与SPP看到的全局顺序(数据流)不同。请注意,SPP是为L2设计的高性能预取器,仅CPLX无法与SPP的效率相提并论(苹果与橙子)。CPLX的实现是非常轻量级的,因为它是一个L1-D预取器,并具有在L1发出预取的关键路径上减少延迟的额外好处(SPP必须通过使用逻辑或查找表来计算置信度)


3. 全局流预取器(Global Stream, GS)

image-20250425163719918

  • 目标场景
    处理由控制流预测的全局数据流,即多个IP访问同一密集内存区域(如流式应用的连续但乱序访问C0,C2,C1,C3,C6,C4)。
  • 实现机制
    • 区域流表(RST)
      • 跟踪2KB区域内的访问密度(32-bit位向量记录已访问缓存行)。
      • 当区域访问密度超过阈值(75%缓存行被访问),标记为“密集区域”。
    • 方向预测:通过饱和计数器判断访问方向(正向/反向)。
    • 跨区域预测:若前一个区域为密集区域,则新区域假设为密集(控制流预测数据流)。
    • 预取策略:按预测方向连续预取k个缓存行(默认k=6)。
  • 优势
    利用空间局部性,适合流式负载(如多媒体处理、矩阵运算)。

Training phase:

  • 当一个新的region被访问,在RST分配一个入口,如果第一次访问这个cacheline,设置相应的 bit位,并且增加dense-count,last-line-offset也会被存储,

  • 如果dense-count大于阈值,这个区域就是一个经常被访问的区域,此时trained bit会被set,注意,如果已经设置了位向量中的位,则计数器不会递增

  • RST通过饱和计数器统计stream方向,被初始化为2^n/2,通过找到连续两次访问cache的差异来获取方向,((the difference between the last-cache-line-offset and current access-offset within a region)(只有tentative或者trained置高才会去更新方向

  • 当遇到GS IP遇到一个新的区域,查看前一个访问的区域,如果前一个为密集的,新区域的tentaive会被设置,如果前一个region未设置trained,可能是该IP可能不具备GS特性了(可以防止强制缺失)

Trained phase:

如果trained和tentative被设置,我们认为IP为GS IP,设置IP table的stream_valid和direction,

这里的方向其实是决定预取地址从后向前还是从前向后,(这里主要就是为了timeliness)

整体结构

image-20250425190101635

由于IP表是直接映射和标记的,决定保留哪个IP用于预取是一个挑战,因为匹配相同表条目的IP之间可能会有冲突,我们添加了一个额外的字段有效位来保持滞后(图5)。当第一次遇到一个IP时,它被记录在IP表中并设置有效位。当另一个IP映射到同一个条目时,有效位被复位,但前一个条目仍然有效。如果当看到新IP时有效位被重置,则表条目被分配给新IP,并且有效位被再次设置,确保在IP表中跟踪两个竞争IP中的至少一个。

如何解决CSPT表满的情况?

CSPT的stride为7bit,正好128个表项就可以覆盖全部,而且signature为7bit,可以直接索引cspt

IP_Table表多个IP映射同一个如何解决?

IP_TABLE是一个直接映射的表,包含64个条目。每个条目都有一个有效位(Valid bit),用于标记该条目是否有效。当一个IP第一次访问某个条目时,它的信息被记录在该条目中,并且有效位置为1。如果另一个IP映射到同一个条目,有效位会被清除(置为0),但之前的条目信息仍然保留。

具体实现:

  • 当一个IP访问IP_TABLE时,如果该条目有效(Valid bit为1),则直接使用该条目中的信息。
  • 如果该条目无效(Valid bit为0),则检查是否有另一个IP的信息仍然保留在该条目中。如果有,且新的IP需要使用该条目,则新的IP的信息会覆盖旧的IP信息,并将有效位置为1。

使用的预取优先级?

GS>CS>CPLX>NL

GS中的region_id和last-vpage区别?

rstable[cpu][i].region_id == ((trackers_l1[cpu][index].last_vpage << 1) | (trackers_l1[cpu][index].last_line_offset >> 5))

GS的tentative如何设置?

访问前一个region,如果region为trained,设置标志位,如果下一个新的区域访问,遇到该标志位会去置高tentative

GS的前瞻预取不能跨页面,否则直接退出

CPST和RST应该组织为数组结构,在电路上应该为CAM结构,而不是cache的用idx索引

为什么CPST要去根据sig取到地址

为什么他在遇到固定的stride时预取覆盖率不够

遇到这种模式,他会首先认为这个是stream模式,然后只预取前几个,而不是按照stride来预取

To Cross, or Not to Cross Pages for Prefetching?

这项研究的目标是L1D预取器,而不是低级高速缓存预取器,因为一级高速缓存可以直接访问虚拟内存子系统,而不是低级高速缓存。

II. MOTIVATION

1) First-Level Caches and Page-Cross Prefetching:

在虚拟地址空间中容易检测到的模式可能在物理地址空间中难以检测到,因为在虚拟地址空间中相邻的两个地址可能在物理地址空间中相隔很远[48]。一级缓存是VIPT,可以直接访问TLB,但不准确的跨页预取可能会导致5次·内存访问,((最多4次内存访问用于推测性页遍历[12],1次内存访问用于高速缓存预取请求))

2) Lower-Level Caches and Page-Cross Prefetching:

放置在低级高速缓存旁边的预取器(L2C有限责任公司)[18],[20],[42],[48],[49],[71],[77],[92],[93]使用物理地址驱动预取决策,因为这些高速缓存被实现为物理索引的物理标记(PIPT)结构[15]。出于安全原因,这些预取器通常只允许在物理页面边界内预取,因为不能保证物理地址的连续性[89]

学术观点

这些作品通常设计新的预取器,其将高速缓存访问与比先前设计更多的特征相关联,以捕获更独特的模式。学术L1D预取器主要限于页面边界内的预取,并且它们没有针对跨页面边界进行优化,尽管它们可以直接访问虚拟内存子系统(第II-A节)。

工业界

当预取器能够准确地执行跨页预取时,将L1D预取器限制为在页边界内预取提供了次优的收益。虽然学术著作仍然限制L1D预取器在页边界内预取,但供应商允许L1D预取器跨页边界预取[3]、[8]、[29]、[35]。跨页预取需要通过TLB层次结构,并可能启动页遍历,该页遍历在TLB中获取预取块所在页的翻译;第II-A节介绍了跨页预取的优点和缺点。但是,供应商没有提供额外的信息。目前还不清楚供应商是遵循始终允许跨页面预取的静态策略,还是使用跨页面边界协调预取的技术。

image-20250411094640722

image-20250411094652527

图3表示了(I)有用的跨页预取,即在其在高速缓存中的生命周期内提供至少一次命中的预取,以及(ii)无用的跨页预取,I

然后引出图4

image-20250411094740926

有用的预取一般会降低TLB KPKI,无用的预取大幅增加MPKI而导致不必要的内存访问(页表遍历)

A scheme able to accurately enable page-cross prefetching only when is beneficial has the potential to deliver
significant performance enhancements.

PAGE-CROSS PREFETCH FILTERING

提出了新的跨页预取算法:MOKA

(1)预测跨页边界预取的有用性的各种程序特征(例如,PC),(2)在决定是否发布跨页预取时考虑系统状态的各种系统特征(例如,sTLB MPKI),以及(3)在运行时调整决策阈值以确保跨不同执行阶段和工作负荷的准确预测的自适应方案。

image-20250411095229333

图5展示了假设VIPT L1D [12],[15]的MOKA框架的设计和高级操作。

在L1D访问时,L1D预取器产生预取请求。对于每个预取请求,MOKA检查它是否是跨页预取A。跨页过滤器仅在跨页边界B的预取中激活,并决定是否发出相应的请求。

通过跨页过滤器的预取需要通过TLB层次结构,以找到预取块所在的页的转换C。如果所请求的转换不是TLB驻留的,则触发推测性页遍历,以将相应的转换带入TLB层次结构D。

最后,发出跨页预取,最终将相应的预取块存储在L1D中。

Hardware Components of the Page-Cross Filter

Perceptron Predictors

MOKA框架提供散列感知器预测器[17],[85],用存储与选定程序特征相关的感知器权重的权重表(wt)实现;感知器权重由饱和计数器[32]、[35]、[44]-[47]、[58]、[59]、[72]、[85]、[86]实现。页面交叉过滤器为每个选定的节目特征使用一个散列感知器预测器。

Saturating Counters for System Features.

MOKA框架还提供系统特征,即,将跨页预取的有用性与系统状态(例如,TLB压力)相关联的特征。通过在跨页过滤器的决策中考虑系统状态,系统特征有助于预测跨页预取是否有用。系统特征是用饱和计数器实现的;我们称之为系统特征权重。

Virtual Update Buffer (vUB)

MOKA uses vUB for training purposes. vUB is responsible for capturing false negatives,i.e., cases where the Page-Cross Filter erroneously discarded a page-cross prefetch that would have saved a demand L1D miss if it was issued.,

Physical Update Buffer (pUB).

跨页过滤器使用pUB来确保根据所发布的跨页预取的有用性来正确更新权重(对于程序和系统功能)。当跨页预取在驱逐之前服务于至少一个L1D需求访问时,它被认为是有用的;否则被认为没用。为此,pUB条目存储跨页过滤器决定发出的跨页预取的物理地址以及相应的散列索引。请注意,pUB出于训练目的存储物理地址(而不是虚拟地址vUB ),因为它在L1D驱逐时更新权重,并且L1D被物理标记

Adaptive Thresholding Scheme.

为了确定跨页预取的有用性,跨页过滤器将累积权重(程序特征权重和系统特征权重的总和)与阈值进行比较;发出累积权重高于阈值的预取,而丢弃其他预取(第三部分-C1)。由于工作负载的异构性和阶段变化行为,使用静态阈值提供了次优的增益。MOKA框架采用了基于时期的机制(第三节-C3 ),该机制利用各种运行时信息(例如,LLC压力)来调整决策中使用的阈值。

Prediction

image-20250411100942238

预测分为四个阶段:

第一阶段1包括三个步骤。首先,跨页过滤器从当前加载请求中提取一组选定的程序特征(第三部分-D1)。然后,每个特征被散列并用于索引相应的WT。在索引之后,从每个WT中检索权重(wi)。所有程序功能都采用相同的程序。第二阶段2检查在决策中是否需要考虑任何系统特征(第三章-D2)。为此,它计算每个系统特征(例如,sTLB MPKI)的值,并将其与阈值(SFn?图6中的Tsfn如果系统特征值(SFn)超过(或子种子,取决于特征)相关阈值(Tsfn),则在决策中考虑相应的权重(weightSFn)。

第三阶段3将所有考虑的程序和系统特征的权重相加,并生成最终权重(wfinal)。然后,将wfinal与激活阈值Ta 4进行比较。如果wfinal大于Ta,则发出跨页预取;否则它将被丢弃

training

MOKA为每个L1D块增加了一个额外的位,称为跨页位(PCB ),指示该块是否已通过跨页预取在L1D中获取。

image-20250411101455040

在L1D需求未命中1时,在vUB中搜索可能的命中2。vUB命中3表示相应的跨页预取被跨页过滤器错误地丢弃,于是程序和系统权重(正训练),增加了将来发布(允许)相应的跨页预取的概率。

类似地,当需求请求在L1D 4中命中并且命中的块具有其PCB设置时,即,该块由有用的跨页预取获取,则查询pUB 5。在pUB命中6时,命中pUB条目的散列索引被用于增加相应程序和系统特征7的权重

在具有PCB的块的L1D驱逐时,PageCross过滤器检查被驱逐的块在其生命期内是否在高速缓存9中提供了至少一次命中。如果没有,搜索pUB以找到匹配的条目10,因为pUB存储所发布的跨页预取的物理地址和散列索引;这就是为什么pUB将物理地址而不是虚拟地址存储为vUB(第III-B节)的原因。

匹配的pUB条目的散列索引用于减少相应的程序和系统权重(负训练)11,因为没有提供任何命中的被驱逐的L1D块指示跨页过滤器未能将相应的跨页预取分类为无用的。

Adaptive Thresholding Scheme:

image-20250411102748445

图8显示了自适应阈值方案的操作。在一个时期内,收集以下运行时统计数据(图8中的步骤I)并用于调整Ta:有用和无用的跨页预取的数量、IPC、LLC未命中率、ROB压力和L1I MPKI。

即具有非常高的缓存和ROB压力的阶段,并当场调整Ta的值。具体来说,它将Ta设置为高阈值(th ),以仅在以下情况下允许具有非常高置信度ii的跨页预取:(1)存在高ROB压力和许多进行中的L1D未命中,以及(2)跨页预取的准确性已经达到低值(T1)。此外,当存在高L1I压力(L1i MPKI>TL1i) iii时,阈值方案将Ta设置为中间值(tm ),以避免加剧L2C中跨页预取和需求指令访问之间的竞争。最后,在LLC压力非常高的阶段期间,阈值方案禁用跨页预取iv;如果LLC压力下降,则由于vUB的操作,页面交叉预取可能再次被激活(第三节-C2)。

在一个时期结束时,阈值方案潜在地使用在先前时期期间收集的运行时间信息来更新Ta。具体而言,它考虑了跨页预取的准确度v,并且当准确度分别低于阈值T2和T1时,它强制中或高阈值。此外,如果在两个连续时期vi之间跨页预取精度增加(减少),则阈值方案将Ta值增加(减少)1。最后,如果在两个连续时期vii之间存在IPC下降,则阈值方案将Ta设置为tm(如果Ta低于tm)

Bouquet of Features

Program Features:

image-20250411104247992

表1显示,大多数程序特性使用虚拟地址的不同位,PC,以及发出跨页预取时预取器使用的增量。请注意,MOKA框架包含的程序特性不是专用于特定预取器的,而是对使用哪个预取器是透明的。制作利用特定预取器的元数据的专用特征(例如,前瞻)有可能进一步提高跨页过滤器的效率。我们的工作并不关注特定的预取器,而是提供了一个有效的跨页预取的整体方案。

System Features:

程序特性(第三节-D1)对于跨页过滤器的准确性和性能至关重要。但是,他们在做出决策时没有考虑系统状态(例如,TLB/缓存压力)。换句话说,过去被证明有用(无用)的跨页预取在具有不同属性的不同执行阶段可能是无用的(有用的)。MOKA框架考虑了表1中列出的6个系统特性来捕捉这些场景。每个系统功能都是用一个饱和计数器实现的,并在不同阶段监控跨页预取的有效性。例如,在sTLB未命中率高于预定义阈值(SFsTLB_missrate>TsTLB_missrate)的阶段,sTLB未命中率系统功能会监控跨页预取的有效性。

Combining Features:

The feature selection process takes place offline and uses IPC speedup as optimization metric.2

DRIPPER: A Page-Cross Filter Prototype

image-20250411104818933

我们观察到所有滴头配置(表II)都使用一个程序特征和两个系统特征。为所有考虑的预取器选择相同的系统特征(sTLB MPKI,sTLB未命中率),而两个预取器(BOP,IPCP)还共同具有所使用的程序特征(PC⊕Delta).

所选功能背后的基本原理如下:此程序功能使用L1D预取器使用的增量来发布跨页预取,并了解特定增量在用于生成跨页预取请求时是否有益。

PC⊕Delta.该程序特征是通过将PC与预取器用来发出跨页预取的增量进行异或运算来计算的,并提供某个PC是否偏好用于跨页预取的特定增量的信息。

sTLB MPKI。这个系统特性将具有低sTLB MPKI速率(SFsTLB_mpki<TsTLB_mpki)的阶段与跨页预取的有用性相关联。该特性背后的核心思想是,当sTLB MPKI较低时,跨页预取请求在TLB层次结构中命中的概率较高(触发页遍历的概率较低)。因此,如果跨页预取在这些阶段有用/无用,这个特性通过增加最终的累积权重(第3步,图6)使DRIPPER对跨页预取更积极/更不积极。只有当SFsTLB_mpki<TsTLB_mpki时,该系统功能才有助于决策。

sTLB未命中率。该系统特征是对sTLB MPKI系统特征的补充,因为它以sTLB压力高的阶段为目标,而sTLB MPKI系统特征以sTLB压力低的阶段为目标。我们使用sTLB失效率度量而不是sTLB MPKI度量来捕获具有高sTLB压力的相位,因为前者比后者对变化更敏感。此系统功能在sTLB未命中率较高的阶段(SFsTLB_missrate>TsTLB_missrate)测量跨页预取的有用性,以识别其他功能无法捕捉的错过的机会。核心思想是,当sTLB中大多数请求内存访问未命中时,允许L1D预取器触发跨页预取可能会提高sTLB命中率,因为跨页预取会引入页遍历。因此,如果跨页预取在高sTLB未命中率的阶段被证明是有用的,这个特性增加了DRIPPER允许跨页预取的可能性。仅当SFsTLB_missrate>TsTLB_missrate时,才使用此功能。

chipyard如何去分发时钟给每个模块,我该如何添加一个模块时钟?(仿真系统)

下面是大小核心校验的参数,可以看到每个核心都设置了时钟频率,这些时钟频率会组织为一个函数数组,送入config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MyRocketConfig extends Config(
new chipyard.config.WithTileFrequency(100, Some(0)) ++
new chipyard.config.WithTileFrequency(50, Some(1)) ++
new chipyard.config.WithTileFrequency(50, Some(2)) ++
new chipyard.config.WithGCBusFrequency(50) ++
new chipyard.config.WithSystemBusFrequency(100) ++
new chipyard.config.WithSystemBusFrequencyAsDefault ++
new freechips.rocketchip.guardiancouncil.WithGHE ++
new freechips.rocketchip.subsystem.WithAsynchronousRocketTiles(
AsynchronousCrossing().depth,
AsynchronousCrossing().sourceSync) ++
// Crossing specifications
new boom.common.WithNMegaBooms(1, overrideIdOffset=Some(0)) ++
new freechips.rocketchip.subsystem.WithNGCCheckers(GH_GlobalParams.GH_NUM_CORES - 1, overrideIdOffset=Some(1)) ++
new chipyard.config.AbstractConfig
)

之后ClockFrequencyAssignersKey存放着函数对

1
2
3
4
class ClockNameContainsAssignment(name: String, fMHz: Double) extends Config((site, here, up) => {
case ClockFrequencyAssignersKey => up(ClockFrequencyAssignersKey, site) ++
Seq((cName: String) => if (cName.contains(name)) Some(fMHz) else None)
})

接下来看时钟是如何连接的:

首先看WithDividerOnlyClockGenerator,这个模块定义了ref时钟的node,然后时钟的分频在DividerOnlyClockGenerator模块中

这里可以理解为AXI xbar,但只有一个输入,多个输出,dividedClocks.getOrElse(div, instantiateDivider(div))承担了为每个tile分配相应的频率

1
2
3
4
5
6
for (((sinkBName, sinkB), sinkP) <- outClocks.member.elements.zip(outSinkParams.members)) {
val div = pllConfig.sinkDividerMap(sinkP)
sinkB.clock := dividedClocks.getOrElse(div, instantiateDivider(div))
// Reset handling and synchronization is expected to be handled by a downstream node
sinkB.reset := refClock.reset
}

然后这个模块调用了

1
2
val pllConfig = new SimplePllConfiguration(pllName, outSinkParams.members)

也就是pllConfig,这个模块主要是获得sink node的信息(频率等),然后根据ref时钟给出分频系数,也就是上面的div,然后dividedClocks就根据不同的kay或者对应的val(clock)分发给sink,至此完成了ref时钟到sink时钟的分发

那么如何得知ref clock的node从何处传入的呢?通过WithDividerOnlyClockGenerator可以得知他连接的输出IO clock

1
2
3
4
5
6
7
8
9
10
11
12
13
InModuleBody {
val clock_wire = Wire(Input(new ClockWithFreq(dividerOnlyClockGen.module.referenceFreq)))
val reset_wire = Wire(Input(AsyncReset()))
val (clock_io, clockIOCell) = IOCell.generateIOFromSignal(clock_wire, "clock", p(IOCellKey))
val (reset_io, resetIOCell) = IOCell.generateIOFromSignal(reset_wire, "reset", p(IOCellKey))

referenceClockSource.out.unzip._1.map { o =>
o.clock := clock_wire.clock
o.reset := reset_wire
}

(Seq(clock_io, reset_io), clockIOCell ++ resetIOCell)
}

但referenceFreq从何产生的呢?

似乎是tile中的最大值?

如何分发时钟呢?

WithDividerOnlyClockGenerator模块有node连接,这里就是将ref时钟分发给tile

1
2
3
(system.allClockGroupsNode
:= dividerOnlyClockGen.node
:= referenceClockSource)

所以找allClockGroupsNode,该node为trait HasChipyardPRCI的一个node,该node最后会连接到aggregator,而aggregator会分发时钟到两个部分:subsystem和asyncClockGroupsNode,然后chiptop会调用这个connectImplicitClockSinkNode函数从而完成subsystem分发

1
2
3
4
5
6
7
8
9
10
11
12
13
def connectImplicitClockSinkNode(sink: ClockSinkNode) = {
val implicitClockGrouper = this { ClockGroup() }
(sink
:= implicitClockGrouper
:= aggregator)
}
(aggregator
:= frequencySpecifier
:= clockGroupCombiner
:= resetSynchronizer
:= tileClockGater.map(_.clockNode).getOrElse(ClockGroupEphemeralNode()(ValName("temp")))
:= tileResetSetter.map(_.clockNode).getOrElse(ClockGroupEphemeralNode()(ValName("temp")))
:= allClockGroupsNode)

实际上ClockGroupFrequencySpecifier模块对之前的ClockFrequencyAssignersKey作出了处理

这里首先输入了函数数组,之后对ClockParameters进行模式匹配,如果子边没有设置ClockParameters(None),此时搜索这个表,匹配相应的name,给出对应的频率,如果子边匹配到了,那么直接返回子边的ClockParameters参数

每个clock必须由assign name,否则无法完成模式匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def apply(
assigners: Seq[(String) => Option[Double]],
defaultFreq: Double)(
implicit p: Parameters, valName: ValName): ClockGroupAdapterNode = {

def lookupFrequencyForName(clock: ClockSinkParameters): ClockSinkParameters = {
require(clock.name.nonEmpty, "All clocks in clock group must have an assigned name")
val clockFreq = assigners.foldLeft(defaultFreq)(
(currentFreq, candidateFunc) => candidateFunc(clock.name.get).getOrElse(currentFreq))

clock.copy(take = clock.take match {
case Some(cp) =>
println(s"Clock ${clock.name.get}: using diplomatically specified frequency of ${cp.freqMHz}.")
Some(cp)
case None => Some(ClockParameters(clockFreq))
})
}

这里的意思就是如果你你开始没有设置ClockParameters(node传入的ClockParameters为none),那么他会在函数数组找到特定的频率,然后分发给tile

然后查看connectImplicitClockSinkNode,最后又回到了WithDividerOnlyClockGenerator,这一小段代码将node连接到每个tile(只要是basesubsystem就可以分发)

1
2
3
4
5
6
7
8
9
10
11
implicit val p = GetSystemParameters(system)
val implicitClockSinkNode = ClockSinkNode(Seq(ClockSinkParameters(name = Some("implicit_clock"))))
system.connectImplicitClockSinkNode(implicitClockSinkNode)
InModuleBody {
val implicit_clock = implicitClockSinkNode.in.head._1.clock
val implicit_reset = implicitClockSinkNode.in.head._1.reset
system.asInstanceOf[BaseSubsystem].module match { case l: LazyModuleImp => {
l.clock := implicit_clock
l.reset := implicit_reset
}}
}

真的需要大小核心频率不同吗?大小核心如果频率不同,那么他们送入L2Cache的时钟也不同,可能出现问题

Vector Runahead

为什么前面有个Vector?

If runahead were instead to stall on cache misses to generate dependent chain loads, then it could regain performance if it could stall on many at once,传统的预取无法去预取间接访问内存的序列

解决的就是runahead的load chain预取

比如a[b[i]]

image-20250325215328667

如图2(b)所示,假设所有存储器访问在高速缓存中未命中,PRE将发出对阵列a中第一个元素的访问的预取。依赖于该存储器访问的指令不能执行,因此对阵列B的访问和指针值不能被预取。当处理器在提前运行模式期间继续通过指令流时,它将命中对数组A中的第二个元素的访问,为此它将发出另一个预取;不幸的是,对B和数据值的相关访问不能被预取。接下来,对A中的第三个元素发出预取,依此类推。当启动提前运行模式的阻塞加载从主内存返回时,提前运行模式停止。此时,已经预取了对数组A的多次访问,但没有预取数组B的元素,也没有预取相关的数据值。

一旦回到正常模式,由于阵列b的第一次缓存未命中,处理器将很快再次停止。

现代处理器通常具有**硬件跨步预取器,**它应该能够预取对阵列A的跨步访问。如果是这样,当PRE访问阵列A中的元素时,它们将在缓存中命中,因此PRE能够发出对阵列B的第一级间接访问的预取请求,如图2(c)所示。在预运行模式下,PRE会将对阵列B的第一次访问(标记为“1”)转换为预取请求。但是不能预取从属数据值,因为它依赖于对B的访问。对A的下一次访问导致高速缓存中的命中,因为它被跨距预取器成功预取,因此处理器可以计算阵列B中的下一次访问的地址,并发出预取请求(标记为“2”)。同样,不能预取从属数据值。对A的下一次访问是高速缓存命中,然后处理器预取对B的第三次访问(标记为“3”),依此类推。总之,即使跨距预取使提前运行执行能够预取多一级间接寻址,它仍然不能预取第二级(或更高级)

而vec如图2(d)所示。

当启动向量提前运行模式时,对阵列A的多次访问被矢量化,即,在多个感应变量偏移处并行地推测性地发出相同的存储器操作。我们对尽可能多的可用向量宽度进行矢量化,在本例中为8。依赖于数组A中的值的指令也被矢量化,包括对数组B和相关数据值的访问。在提前运行模式期间对相关指令流进行矢量化,同时保持提前运行模式直到发出最后一个相关加载,这使得能够推测性地预取整个相关加载链。向量化提前运行指令流具有在发出下一批并行的相关存储器访问之前,从循环的多次迭代中同时发出相同的存储器操作的效果,等等。与原始指令流相比,这种对存储器访问的有效重新排序使得向量提前运行能够首先向A发出一批访问,然后向B发出一批访问,最后向相关数据值发出一批访问。换句话说,即使独立的内存访问在原始动态指令流中可能相距很远,矢量提前运行也会并行发出它们

优点

矢量化具有两个主要优点:(1)它大大增加了提前运行模式期间的有效提取/解码带宽,即我们一次提取/解码多个循环迭代,以及(2)它需要非常少的后端硬件资源,即向量提前运行模式中的向量指令对应于原始代码中来自多个循环迭代的多个标量指令,同时仅占用单个发布队列槽

具体设计

image-20250325222837702

跨距检测器19用于查找代码中的常规访问模式,这些模式可用作“归纳变量”来生成代码的推测性矢量化副本。一旦我们进入向量提前运行模式(第III-C节),依赖于这种矢量化步长模式的指令就被污点向量跟踪(第III-D节),并被矢量化(第III-E节):地址计算算术运算被转换为向量单元运算,依赖者将自己加载到向量集合中。假设分支在每个矢量化副本上匹配,使用屏蔽来处理其他情况(第III-F节)。为了进一步将存储器级并行性提高到比单个向量加载所支持的水平更高的水平,我们设计了向量展开和流水线技术(第III-G节)来同时发出许多未来的加载。由于这导致旧标量指令和新向量指令之间的一对多关系,所以在前端引入向量寄存器分配表(VRAT)用于寄存器分配(第III-H节),在后端引入寄存器解除分配队列(RDQ)(第III-I节)。

Detecting Striding Loads

为了检测代码中的序列,我们可以从中生成归纳变量来对未来的内存访问进行矢量化,我们使用了一个简单的参考预测表[19,62],它会在每个加载指令执行后进行更新。这由加载PC索引,并且每个条目维护四个字段:(1)最后访问的存储器地址;(2)负载的最后观察到的步幅;(3)指示置信度的2位饱和计数器;以及(4)来自跨步加载的指令链中的最终相关加载的终止符或PC。

这里类似于分支预测器,学习stride,相当于一个简单的stride学习器

最后一个字段(4)是新的,是在一轮矢量提前运行期间填充的,它允许我们在矢量提前运行模式下完成所有有用的工作后提前终止(第III-J节)。

Entering Vector Runahead

在加载指令阻塞ROB的头部之后,当满足以下两个条件中的任何一个时,内核进入runahead模式:(1)ROB被指令填充;或者(2)发布队列被填充到其全部容量的80%。

除了为从分支错误预测中恢复而存储的检查点之外,向量提前运行还通过为前端(RAT)的每个条目存储一个检查点来检查PC和前端RAT。这标志着进入提前运行模式。当我们返回正常模式时,处理器状态将恢复到该检查点。

这里解释了如何进入runahead,以及进入runahead需要保存的状态

我们访问每个加载指令的步距检测器。直到我们达到一个跨步负载,或者如果不存在这样的跨步负载,向量Runahead与PRE [64]类似地执行,但是不使用其完全关联的停止切片表,向量Runahead消除了对其的需要,以避免在没有这种模式的情况下损害工作负载,并捕获向量Runahead稍后使用的任何标量依赖关系。

没stride load就和pre执行差不多

当我们解码一个跨越负载(置信度= 3)时,进入矢量提前运行模式开始。我们对跨越的负载进行矢量化,随后是依赖于它的指令序列。当检测到同一跨步负载的另一个动态实例时,或者从属链完成时,该过程终止(第III-J节)。我们称两个动态跨越负载实例之间的依赖指令为间接链。

Taint Vector

为了跟踪哪些操作(传递地)依赖于指令流中新矢量化的跨负载,我们使用污点向量(TV)。

这为每个架构整数寄存器提供了一个条目,并存储两个标志:(1)如果写入该寄存器的前一个指令是向量化操作(向量化位);以及(2)如果先前写入该寄存器的指令无效(无效位)。TV在runahead开始时是空的,因为它在runahead终止时被清除。

向量化位最初是为所发现的跨越式加载的目标体系结构寄存器设置的。无效位最初基于不支持的操作的目的地来设置,例如,那些将浮点操作作为输入的操作(其总是无效的,因此不需要TV条目)。

如果任何一个指令的输入寄存器被标记,那么目标寄存器也被标记。如果没有输入寄存器被标记,则目标寄存器的标志被复位。

没有设置位的指令作为传统的标量提前运行操作发出,并且在当前的矢量提前运行模式迭代中,相对于指令序列的矢量化副本被视为循环不变的。

具有无效位组的指令被丢弃,只有矢量化位组的指令被矢量化。

Vectorizing Instructions

在提前运行模式下执行的指令只在产生存储器访问时有用,它们的状态不在ROB中维护。因此,在runahead模式下不分配任何ROB条目。相反,我们使用更简单的寄存器解除分配队列[64] (RDQ,第III-I节)来处理寄存器可用性。由于浮点指令很少用于计算地址本身,我们忽略了这种指令(将它们和任何使用它们的指令标记为无效[57]),以及存储和任何已经在原始代码中矢量化的指令。

Control Flow

当向量化时,我们隐含地假设所有向量通道将遵循彼此相同的控制流模式。然而,当在向量提前运行模式下执行时,当通道遇到分支指令时,它们之间可能存在分歧。我们使用微操作将标量分支转换为八个向量通道的谓词掩码。由于向量提前运行不需要覆盖所有代码,因此我们仅使用第一个通道的结果来确定分支的方向,并屏蔽掉任何将采用不同控制路径的通道。这种屏蔽一直持续到我们终止vector-runahead的单次迭代。相比之下,单个向量提前间隔内的不同展开迭代(第III-G节)可以遵循独立的控制流

Vector Unrolling and Pipelining

向量提前运行使用两种技术:向量展开和向量流水线,通过增加提前运行的程度来提高性能,以允许比指令集体系结构本身支持的向量更宽的向量。

image-20250326155008172

Vector RAT

我们在架构标量寄存器和重命名的物理向量寄存器之间有一对多的关系。对于深度为4的流水线,我们需要将一个架构标量寄存器重命名为4个独立的向量物理寄存器(每个寄存器包含8个数据元素)。这意味着我们添加一个新的向量寄存器分配表(VRAT ),每个架构整数寄存器有P个条目,记录分配给指令的P个流水线副本的P个目标物理向量寄存器。当我们在VRAT中查找这些P寄存器时,新矢量化指令的P个副本中的每一个都使用P个条目中的一个作为自己的输入。这使我们能够区分向量流水线配置中的独立流水线迭代的输入和输出,从取指令的角度来看,它们都是同一指令的别名。16个整数寄存器中的每一个都需要P个条目,这通常是很小的

Managing Pipeline Resources During Runahead

必须有足够数量的未使用的发布队列和物理(标量和向量)寄存器文件条目,用于推测性地执行导致间接加载的间接链。在矢量提前运行中,一条矢量指令从分派到执行占用一个issue入口。在执行时,发布队列条目被释放,并且可以被分配给更年轻的指令,类似于标准的OoO内核。

如何去保存映射关系与释放映射关系?

这是通过一个简单的有序寄存器解除分配队列(RDQ)来完成的,PRE [64]也使用这个队列。在VRAT中查找每条指令,以查看保存最后写入同一架构寄存器的指令的目的地的P(矢量流水线下)物理矢量寄存器,一旦新指令到达流水线的末端,该寄存器就会失效。

这个相当于一个小rob

Terminating Runahead

当满足以下四个条件中的任何一个时,矢量提前运行模式终止:(1)我们再次遇到相同跨越负载的动态实例;(2)我们遇到并发出终止符:由步幅检测器(第III-B节)识别为序列中最后一个相关加载的PC;(3)所有向量车道都已被标记为无效;或者(4)在沿着意外的代码路径行进的情况下,我们超时(在向量提前运行模式下执行了200个标量等价指令之后)。当U > P(截面III-G)时,即展开长度大于管道深度时,我们立即重新进入矢量提前运行模式,加载下一个跨越负载

再次发布矢量集合。重复这一过程,直到我们发出了U=P个总回合,然后才恢复正常执行。如第六节所示,对整个间接链进行矢量化的好处远远超过内核处于提前运行模式的额外时间,因为矢量提前运行比典型的无序执行产生更高的内存级并行性。

终止后,我们将前端RAT恢复到进入runahead模式的点,TV、VRAT和RDQ被清除。前端被重定向以从ROB中最后调度的指令之后的下一条指令获取。

A Top-Down Method for Performance Analysis and Counters Architecture

为什么要提出这个方法学?

传统的性能评估方法:

image-20250319103349042

其限制如下:

  1. Stalls overlap:OoO处理器许多模块都是并行计算,可能dcache miss惩罚会被overlap(即使miss也可以执行其他没有依赖的指令)
  2. Speculative execution:推测执行产生的错误结果不会影响提交,他们对性能实际影响较小(相对正确路径)
  3. Penalties are workloaddependent:性能惩罚的周期不定,而是和具体的工作负载相关,传统的性能评估方法认为所有惩罚周期一致
  4. Restriction to a pre-defined set of miss-events:只有一些比较常见的会被评估,但比较细节的就可能被忽视(前端供指不足)
  5. Superscalar inaccuracy:超标量处理器可以一个周期执行退休多条指令,issue带宽可能会出现问题(其中一个例子)

于是,intel提出了top-down的性能分析方法,该方法不仅可以应用于微结构探索,也可以去应用于软件分析

他的分析方法就相当一个树,从跟节点到叶子节点一点点分析(选择占比大的事件分析),特点如下:

  1. 引入错误预测的性能事件,并放在性能事件顶部

  2. 引入专门的计数器(12个)

  3. 确定关键的流水线阶段(issue)和计数时机

    比如,与其统计内存访问总时间,不如去观察由于mem access hang导致的执行单元利用率低的持续时间

  4. 使用通用的事件

其分析层次如下:

image-20250326171600522

我们选择issue阶段来进行事件分解

将性能事件分为4类: Frontend Bound, Backend Bound, Bad Speculation and Retiring

image-20250326171657191

前端 bound

前端分为Latency和Bandwidth,

Latency:icache未命中等导致前端无法产生有效指令

Frontend Bandwidth Bound:decoder效率低,其次就是取指令宽度不够(后端执行太快)

boom很难产生Latency bound,后端执行单元少,无法利用OoO的并行

Bad Speculation category

细分为br mispred和mechine clear

Backend Bound category

这里可能会有dcache miss divider执行等情况

其进一步细分为Memory Bound 和Core Bound

为了维持最大IPC,需要去在4-wide的机器上尽量让exu跑满

Memory Bound:显而易见,就是lsu所造成的性能损失

Core Bound :执行端口利用率低,除法指令

下面还展示了将Mem Bound细分

image-20250326173425211

计数器结构

image-20250326173528480

有*号代表现代的PMU含有该计数器,可以看到只需8个新的perf计数器就可以实现一级level的TMA,其计算公式如下:

image-20250326173651209

image-20250326173659628

Rocket chip 性能计数器

RISCV只给出了性能计数器的标准,但并未给出性能计数器的具体实现,rocket chip 的实现方法如下:

这里的EventSets,给出了三个EventSet,每个EventSets通过event选择

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
val perfEvents = new EventSets(Seq(
new EventSet((mask, hits) => Mux(wb_xcpt, mask(0), wb_valid && pipelineIDToWB((mask & hits).orR)), Seq(
("exception", () => false.B),
("load", () => id_ctrl.mem && id_ctrl.mem_cmd === M_XRD && !id_ctrl.fp),
("store", () => id_ctrl.mem && id_ctrl.mem_cmd === M_XWR && !id_ctrl.fp),
("amo", () => usingAtomics.B && id_ctrl.mem && (isAMO(id_ctrl.mem_cmd) || id_ctrl.mem_cmd.isOneOf(M_XLR, M_XSC))),
("system", () => id_ctrl.csr =/= CSR.N),
("arith", () => id_ctrl.wxd && !(id_ctrl.jal || id_ctrl.jalr || id_ctrl.mem || id_ctrl.fp || id_ctrl.mul || id_ctrl.div || id_ctrl.csr =/= CSR.N)),
("branch", () => id_ctrl.branch),
("jal", () => id_ctrl.jal),
("jalr", () => id_ctrl.jalr))
++ (if (!usingMulDiv) Seq() else Seq(
("mul", () => if (pipelinedMul) id_ctrl.mul else id_ctrl.div && (id_ctrl.alu_fn & aluFn.FN_DIV) =/= aluFn.FN_DIV),
("div", () => if (pipelinedMul) id_ctrl.div else id_ctrl.div && (id_ctrl.alu_fn & aluFn.FN_DIV) === aluFn.FN_DIV)))
++ (if (!usingFPU) Seq() else Seq(
("fp load", () => id_ctrl.fp && io.fpu.dec.ldst && io.fpu.dec.wen),
("fp store", () => id_ctrl.fp && io.fpu.dec.ldst && !io.fpu.dec.wen),
("fp add", () => id_ctrl.fp && io.fpu.dec.fma && io.fpu.dec.swap23),
("fp mul", () => id_ctrl.fp && io.fpu.dec.fma && !io.fpu.dec.swap23 && !io.fpu.dec.ren3),
("fp mul-add", () => id_ctrl.fp && io.fpu.dec.fma && io.fpu.dec.ren3),
("fp div/sqrt", () => id_ctrl.fp && (io.fpu.dec.div || io.fpu.dec.sqrt)),
("fp other", () => id_ctrl.fp && !(io.fpu.dec.ldst || io.fpu.dec.fma || io.fpu.dec.div || io.fpu.dec.sqrt))))),
new EventSet((mask, hits) => (mask & hits).orR, Seq(
("load-use interlock", () => id_ex_hazard && ex_ctrl.mem || id_mem_hazard && mem_ctrl.mem || id_wb_hazard && wb_ctrl.mem),
("long-latency interlock", () => id_sboard_hazard),
("csr interlock", () => id_ex_hazard && ex_ctrl.csr =/= CSR.N || id_mem_hazard && mem_ctrl.csr =/= CSR.N || id_wb_hazard && wb_ctrl.csr =/= CSR.N),
("I$ blocked", () => icache_blocked),
("D$ blocked", () => id_ctrl.mem && dcache_blocked),
("branch misprediction", () => take_pc_mem && mem_direction_misprediction),
("control-flow target misprediction", () => take_pc_mem && mem_misprediction && mem_cfi && !mem_direction_misprediction && !icache_blocked),
("flush", () => wb_reg_flush_pipe),
("replay", () => replay_wb))
++ (if (!usingMulDiv) Seq() else Seq(
("mul/div interlock", () => id_ex_hazard && (ex_ctrl.mul || ex_ctrl.div) || id_mem_hazard && (mem_ctrl.mul || mem_ctrl.div) || id_wb_hazard && wb_ctrl.div)))
++ (if (!usingFPU) Seq() else Seq(
("fp interlock", () => id_ex_hazard && ex_ctrl.fp || id_mem_hazard && mem_ctrl.fp || id_wb_hazard && wb_ctrl.fp || id_ctrl.fp && id_stall_fpu)))),
new EventSet((mask, hits) => (mask & hits).orR, Seq(
("I$ miss", () => io.imem.perf.acquire),
("D$ miss", () => io.dmem.perf.acquire),
("D$ release", () => io.dmem.perf.release),
("ITLB miss", () => io.imem.perf.tlbMiss),
("DTLB miss", () => io.dmem.perf.tlbMiss),
("L2 TLB miss", () => io.ptw.perf.l2miss)))))

rocketchip的性能计数器需要写入hpmevent来给出对应的操作:

具体的,hpmevent低8位选择不同的EventSet,其他位去选择具体的性能事件,比如向mhpmevent3写入0x4200,那么mhpmcounter在发生int load commit或 cond branch inst commit均会递增

image-20250324161603855

下面介绍一些函数的用法:该片段节选自CSR,这里主要就是去写入计数器(用于初始化)和event,注意在写入event时需要执行maskEventSelector,防止写入非法的区域

1
2
3
4
5
for (((e, c), i) <- (reg_hpmevent zip reg_hpmcounter).zipWithIndex) {
writeCounter(i + CSR.firstMHPC, c, wdata)//写入计数器
when (decoded_addr(i + CSR.firstHPE)) { e := perfEventSets.maskEventSelector(wdata) }//写入计数器使能
}

具体的,eventSetIdBits为8,set Mask是去mask不同的EventSet,maskMask是去mask一个EventSet不同的性能事件,然后执行eventSel & (setMask | maskMask).U保证写入一定是合法的值

1
2
3
4
5
6
7
//这里低8位是用来选择模式的(也就是不同的性能计数器),setMask的意思就是给出此次选择哪个eventSet的Mask
def maskEventSelector(eventSel: UInt): UInt = {
// allow full associativity between counters and event sets (for now?)
val setMask = (BigInt(1) << eventSetIdBits) - 1
val maskMask = ((BigInt(1) << eventSets.map(_.size).max) - 1) << maxEventSetIdBits
eventSel & (setMask | maskMask).U
}

如何去触发性能事件并且使得相应的计数器增加呢?

首先通过写入reg_hpmevent,配置好性能事件

然后在rocketchip的核心中有

1
csr.io.counters foreach { c => c.inc := RegNext(perfEvents.evaluate(c.eventSel)) }

也就是遍历每个event是否触发了性能事件,并且给出增量,该增量会传入CSR,代码如下:

1
2
val reg_hpmcounter = io.counters.zipWithIndex.map { case (c, i) =>
WideCounter(CSR.hpmWidth, c.inc, reset = false, inhibit = reg_mcountinhibit(CSR.firstHPM+i)) }

reg_mcountinhibit控制对应的计数器是否递增,为1禁止,(这里计数器最大为40位,因为64位会浪费bit)

接下来讲解evaluate是如何工作的,下面代码是EventSets类的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private def decode(counter: UInt): (UInt, UInt) = {
require(eventSets.size <= (1 << maxEventSetIdBits))
require(eventSetIdBits > 0)
(counter(eventSetIdBits-1, 0), counter >> maxEventSetIdBits)
}

def evaluate(eventSel: UInt): Bool = {
val (set, mask) = decode(eventSel)
val sets = for (e <- eventSets) yield {
require(e.hits.getWidth <= mask.getWidth, s"too many events ${e.hits.getWidth} wider than mask ${mask.getWidth}")
e check mask
}
sets(set)
}

decode给出的是set(选择了哪个EventSet),mask(具体性能事件的选择),然后遍历了eventSets,对每个元素执行check函数

1
2
3
4
5
6
def size = events.size
val hits = WireDefault(VecInit(Seq.fill(size)(false.B)))
def check(mask: UInt) = {
hits := events.map(_._2())
gate(mask, hits.asUInt)
}

check函数主要就是根据EventSet传入的函数,给出是否命中的信息,最后sets(set)就是选择对应的set,

rocket chip的性能计数器设置每个周期只能+1,这样就无法去将该逻辑应用到BOOM中

  • 兼容boom

现在已经兼容boom,并添加了一级level的TMA

0%