XiangShan 数据预取器架构代码解读
XiangShan Prefetcher RTL 实现
核心设计思想是:
- 上层预取器可以跨层级”指示”下层预取(L1 可以让 L2 预取,L1 可以让 L3 预取),但这种跨层指示走的是 专用旁路通道(sideband,BundleBridge),而不是 TileLink 缓存一致性总线上的 Hint 报文;
- 每个预取请求都携带”来源标识”(
pfSource/MemReqSource),使得请求在跨层传递、在下层流水线处理、以及在性能统计时都能追溯到是哪一级、哪一个预取器发起的; - 预取深度按层级递增:越靠近核心的层级(L1)预取得越”近”(lookahead 小),越远的层级(L2/L3)预取得越”远”(lookahead 大),以匹配各级 miss 延迟差异
各层级使用的预取器:
| 层级 | 算法 | 说明 |
|---|---|---|
| L1 D-Cache | Stride、Stream、SMS(Spatial Memory Streaming) | Stride/Stream 由 L1Prefetcher 统一管理;SMS 是独立模块 |
| L2(coupledL2) | BOP(Best-Offset Prefetch,含虚拟地址 VBOP + 物理地址 PBOP)、TP(Temporal Prefetch)、PrefetchReceiver(接收 L1 提示) | 默认启用 BOP + 接收器,TP 可选 |
| L3(huancun) | PrefetchReceiver(接收上层提示,默认)、BOP(代码存在但默认不启用)、TPmeta(TP 的元数据存储介质) | L3 默认只做”接收 + 存 TP 元数据”,自身一般不主动生成预取 |
⚠️ 默认配置下, L3 不是用自己的预取器去预取,而是 接收 L1 通过旁路送来的预取地址 并执行,外加 充当 L2 时序预取(TP)的元数据存储后端
三类数据通路
跨层级预取请求一共有三类完全不同的物理通路
1 | ┌─────────────────────────────────────────────────────────────────────────────┐ |
预取来源与目标层级的编码体系
MemReqSource —— 全局内存请求来源枚举
文件:utility/.../BusKeyField.scala:24-45
1 | object MemReqSource extends Enumeration { |
- 整个 SoC 用统一的 4-bit 编码标识一笔内存访问的来源,预取相关取值从
8(Prefetch2L2BOP)到15(Prefetch2L3Unknown)。 - 通过 TileLink 的
ReqSourceKeyuser 字段在总线上传递
1 | // BusKeyField.scala:48-52 |
L1PrefetchSource —— L1 内部的预取来源(3 bit)
文件:L1PrefetchInterface.scala:30-47(trait HasL1PrefetchSourceParameter)
1 | L1_HW_PREFETCH_NULL = 0 未被预取 |
- 这是 L1 D-Cache 内部用来标记一个 cache line 是被哪个算法预取进来的(保存在 cache 的 meta 里),用于训练反馈与 FDP 统计。
- 当 L1 要把预取请求送到 L2 时,会把这个内部编码 映射 成
MemReqSource编码
PfSource —— L2 内部的预取来源(3 bit)
文件:coupledL2/prefetch/PrefetchParameters.scala:39
1 | object PfSource extends Enumeration { |
- L2 内部用更紧凑的 3-bit
PfSource表示来源(区分 SMS/BOP/PBOP/Stream/Stride/TP) fromMemReqSource()负责把外部传来的 4-bitMemReqSource翻译成 L2 内部的 3-bitPfSource
SINK_L1 / SINK_L2 / SINK_L3 —— 目标层级标记(2 bit)
文件:L1PrefetchComponent.scala
1 | SINK_L1 = 0b00 预取到 L1 D-Cache |
- 这是 L1 预取器内部 用来标记”这条预取请求应该落在哪一级”的字段,保存在多级过滤表
MLPReqFilterBundle.sink中 - L1 的过滤/仲裁逻辑会根据
sink把请求分发到三个不同的输出端口(io.l1_req/io.l2_req/io.l3_req)
TileLink A 通道上承载预取语义的 user 字段
当预取真正变成一笔下行内存访问(通路 B)时,预取语义被塞进 TileLink A 通道的 user/echo 字段:
| 字段(Key) | 含义 | 典型来源 |
|---|---|---|
ReqSourceKey |
MemReqSource 编码(4 bit),标识请求来源 |
L1 MissQueue 写入 |
VaddrKey |
虚拟地址高位(用于下层预取器做虚拟地址训练) | L1/L2 写入 |
PrefetchKey |
是否需要下层回送预取 hint | L1 写入 |
AliasKey |
Cache 别名位(VIPT 别名消歧) | L1 写入 |
IsKeywordKey(echo) |
关键字优先(critical-word-first) | L1 写入 |
注意区分:
ReqSourceKey走的是 TileLink(通路 B),而预取”指示”用的pf_source走的是 PrefetchRecv 旁路(通路 A)。两者都用MemReqSource这套数值,但物理载体不同,不要混淆。
L1 数据预取器
L1 数据侧实际上例化了 两个独立的预取器实例(MemBlock.scala:413-460):
1 | // 实例 1:SMS(Spatial Memory Streaming)—— 只向 L2 预取 |
两个实例的分工:
prefetcherOpt(SMS):擅长识别”空间访问模式”(一个 region 内不规则但稳定的访问足迹),只输出到 L2(io.l1_req.ready被钉死为false)。l1PrefetcherOpt(L1Prefetcher):内含 Stream + Stride 两个子算法,同时输出 L1 / L2 / L3 三条预取流
基类与接口:BasePrefecher / PrefetcherIO
文件:BasePrefecher.scala:58-72
1 | class PrefetcherIO()(implicit p: Parameters) extends XSBundle { |
三个输出端口的本质区别:
l1_req是DecoupledIO(有ready反压)——因为它要去抢占宝贵的 L1 D-Cache load 流水线端口,必须能被反压;l2_req/l3_req是ValidIO(无反压,即生即发)——它们只是”提示”下层,丢了也无所谓(下层有自己的过滤队列)
训练输入 bundle PrefetchReqBundle(BasePrefecher.scala:74-80):
1 | class PrefetchReqBundle extends XSBundle { |
Stride 预取器
文件:L1StridePrefetcher.scala
核心数据结构 StrideMetaBundle(L1StridePrefetcher.scala:58-110):
1 | class StrideMetaBundle extends XSBundle { |
关键参数(L1StridePrefetcher.scala:40-56):
| 参数 | 值 | 含义 |
|---|---|---|
STRIDE_ENTRY_NUM |
10 | 训练表条目数(以 PC 哈希索引) |
STRIDE_CONF_BITS |
2 | 置信度位宽,MAX_CONF = 3 |
STRIDE_LOOK_AHEAD_BLOCKS |
2 | 基础预取深度 |
l1_stride_ratio |
2 | L1 预取深度 = stride << 2(约 4 倍步长) |
l2_stride_ratio |
5 | L2 预取深度 = stride << 5(约 32 倍步长) |
置信度机制与发射条件(update() 方法):
- 新步长
new_stride = new_vaddr - pre_vaddr; - 若
new_stride == stride,confidence++(上限MAX_CONF=3); - 否则
confidence--,且仅当confidence ≤ 1时才替换stride(低置信下才允许切换步长,避免抖动); - 只有
confidence == MAX_CONF时才允许发射预取。
L1/L2 双深度发射(4 级流水 S0–S4,L1StridePrefetcher.scala:191-233):
1 | val s2_l1_depth = s2_stride << l1_stride_ratio // 近距离 |
- S3 输出 L1 预取请求,S4 输出 L2 预取请求;
- 与 Stream 的协同:S0 会向 Stream 表发起
stream_lookup_req,若该地址已被 Stream 识别为活跃流,则抑制 Stride 的 L1 输出(避免两个算法重复预取同一区域)。
Stream 预取器
文件:L1StreamPrefetcher.scala
核心数据结构 StreamBitVectorBundle(L1StreamPrefetcher.scala:54-111):
1 | class StreamBitVectorBundle extends XSBundle { |
关键参数(L1StreamPrefetcher.scala:16-52):
| 参数 | 值 | 含义 |
|---|---|---|
BIT_VEC_ARRAY_SIZE |
16 | 流检测表条目数 |
BIT_VEC_WIDTH |
16 | 一个 region 含 16 个 cache block |
ACTIVE_THRESHOLD |
BIT_VEC_WIDTH - 4 = 12 |
region 内命中超过 12 块即激活该流 |
DEPTH_CACHE_BLOCKS |
16 | L1 预取深度(基础 16 块 = 1KB) |
WIDTH_CACHE_BLOCKS |
2 | 每次预取宽度 2 块 |
L2_DEPTH_RATIO |
2 | L2 深度 = L1 深度 × 2 |
L3_DEPTH_RATIO |
3 | L3 深度 = L1 深度 × 3 |
工作原理(5 级流水 S0–S5):
- S0 邻域检测:对当前访问,同时检查
region_tag、region_tag+1、region_tag-1三个 region。只有当相邻 region 存在活跃流时,才认为本次访问是流的延续; - S1 分配/更新:若是新流,根据命中的是 +1 还是 -1 邻域决定
decr_mode(正向流 / 反向流); - S2 生成三级预取地址:
1
2
3s2_l1_vaddr = s2_vaddr + dynamic_depth // L1
s2_l2_vaddr = s2_vaddr + (dynamic_depth << ratio) // L2
s2_l3_vaddr = s2_vaddr + (dynamic_depth << l3_ratio)// L3 - S3/S4/S5 分别输出 L1 / L2 / L3 预取请求;
dynamic_depth是 运行时可调 的(来自 FDP 反馈,见第 10 节)。
Stream 是唯一会主动产生 L3 预取(
io.l3_req)的 L1 子算法——它在最远的L3_DEPTH_RATIO=3深度上预取,把数据提前拉到 L3。
SMS 预取器(Spatial Memory Streaming)
文件:SMSPrefetcher.scala(约 1358 行,是 L1 最复杂的预取器)
SMS 针对 空间局部性模式:把虚拟地址空间划分为固定大小的 region,学习”访问了 region 内某个偏移后,通常还会访问哪些偏移”的足迹(footprint)。
Region 概念(SMSPrefetcher.scala:63-101):
1 | 虚拟地址布局: | REGION_TAG | REGION_OFFSET(4b) | BLOCK_OFFSET(6b) | |
五大子模块:
StridePF(SMSPrefetcher.scala:139-257):SMS 内置的 stride 检测器(16 条目,带符号 stride,2 级置信度),用于捕捉 region 间的规则步长。ActiveGenerationTable (AGT)(SMSPrefetcher.scala:286-567):活跃生成表,跟踪当前正在被访问的 region,累积其访问位图region_bits。当一个 region “成熟”(访问计数超过act_threshold,默认 12)时,把它的足迹”毕业”到 PHT。表项AGTEntry含region_bits(已访问块)、region_tag、access_cnt、decr_mode等。PatternHistoryTable (PHT)(SMSPrefetcher.scala:584-907):模式历史表(64 项 × 2 路,SRAM 实现),以 2-bit 饱和计数器 记录每个 region 内各块的访问概率。当新访问命中 PHT 时,按计数器预测”接下来要访问哪些块”,可同时生成当前 region、下一 region(incr)、上一 region(decr)三种预取模式。PrefetchFilter(SMSPrefetcher.scala:920-1112):预取过滤表(16 项),负责 V→P 翻译(发 TLB 请求)、PMP 权限检查、去重,并把 region 位图按块逐个发射为最终预取请求。Drop 条件包括 page/access/guest-page fault、uncache/MMIO、PMP 拒绝。SMSTrainFilter(SMSPrefetcher.scala:1114-1214):训练流去重(8 项 FIFO,块级去重,保持程序序)。
SMS 的输出(关键)(SMSPrefetcher.scala:1326-1336):
1 | // SMS 只向 L2 发预取 |
即:SMS 的预取目标固定是 L2,来源标记为
Prefetch2L2SMS(数值 10)。
L1 预取顶层整合:L1PrefetchComponent.scala
文件:L1PrefetchComponent.scala(约 923 行)
核心是 MutiLevelPrefetchFilter(多级预取过滤器)(L1PrefetchComponent.scala:254-843),它把 Stream 和 Stride 的请求汇聚,并维护 两张过滤表:
- L1 过滤表(
MLP_L1_SIZE = 16):只处理sink == SINK_L1的请求; - L2/L3 过滤表(
MLP_L2L3_SIZE = 16):处理sink == SINK_L2 / SINK_L3的请求。
过滤表项 MLPReqFilterBundle(L1PrefetchComponent.scala:254-359)关键字段:
1 | val bit_vec = UInt(BIT_VEC_WIDTH.W) // 待预取块位图 |
三路输出分发(L1PrefetchComponent.scala:700-820):
1 | // → L1:DecoupledIO,有反压,逐块发送 |
预取器优先级(L1PrefetchComponent.scala:890-903):在 L1 和 L2/L3 两条仲裁路径上,Stream 优先级都高于 Stride:
1 | pf_queue_filter.io.l1_prefetch_req.bits := Mux( |
L1 预取来源到 MemReqSource 的映射小结
L1 内部(L1PrefetchSource) |
映射到(MemReqSource) |
目标 |
|---|---|---|
L1_HW_PREFETCH_STRIDE (2) |
Prefetch2L2Stride (12) |
L2 |
L1_HW_PREFETCH_STREAM (3) |
Prefetch2L2Stream (11) |
L2 |
| SMS(独立模块,直接赋值) | Prefetch2L2SMS (10) |
L2 |
L1 → L2 / L1 → L3 通路(旁路 sideband)
载体:PrefetchRecv Bundle
L2 侧定义(coupledL2/Common.scala:345):
1 | class PrefetchRecv extends Bundle { |
L3 侧定义(huancun/Common.scala:234,少一个 pf_source):
1 | class PrefetchRecv extends Bundle { |
这两个 Bundle 通过 BundleBridgeSource / BundleBridgeSink 点对点连接——这是 Diplomacy 框架里一条 独立于 TileLink 缓存总线 的旁路(sideband),专门用来传”预取地址提示”。
L1 → L2:在 MemBlock 中仲裁合并
文件:MemBlock.scala:593-609
1 | prefetcherOpt.foreach(sms_pf => { // sms_pf = SMS 预取器 |
要点:
- SMS 与 Stream/Stride 的 L2 预取请求 在 MemBlock 里合并成一条
l2_pf_sender旁路输出; - 仲裁规则:Stream/Stride(
l1_pf)优先于 SMS(sms_pf)(Mux条件以l1_pf_to_l2.valid为先); pf_source字段一并传递,下层据此知道是 SMS / Stream / Stride;- 性能计数器
sms_block_by_l1pf专门统计”SMS 被 L1Prefetcher 抢占”的次数(MemBlock.scala:625)。
L2 顶层接收(CoupledL2.scala:362-366):
1 | pf_recv_node match { |
L1 → L3:跨核汇聚到 L3
文件:MemBlock.scala:611-613
1 | val l1_pf_to_l3 = ValidIODelay(l1_pf.io.l3_req, 4) // 只有 L1Prefetcher(Stream) 的 L3 请求,延迟 4 拍 |
要点:
- 只有 L1Prefetcher(即 Stream,因为 Stride 不发 L3) 的
io.l3_req会通过l3_pf_sender旁路发出;SMS 不发 L3; - 延迟 4 拍(比 L2 通路的 2 拍更长,匹配 L3 更远的距离);
- 注意 L3 的
PrefetchRecv没有pf_source字段,所以到 L3 时来源信息丢失
SoC 顶层把各个核的 L3 预取请求 汇聚 到 L3(Top.scala:376-382):
1 | l3.pf_recv_node match { |
L2/L3 接收端:PrefetchReceiver
L2 PrefetchReceiver(coupledL2/prefetch/PrefetchReceiver.scala:36-60)——透传来源:
1 | io.req.bits.tag := parseFullAddress(io.recv_addr.bits.addr)._1 |
L3 PrefetchReceiver(huancun/prefetch/PrefetchReceiver.scala:20-33)——来源被硬编码:
1 | io.req.bits.tag := parseFullAddress(io.recv_addr.bits)._1 |
L2 预取器(coupledL2)
文件:coupledL2/prefetch/Prefetcher.scala(顶层)
L2 预取顶层 Prefetcher 把多个预取源汇聚成一条统一的预取请求流。默认配置(见第 12 节)下包含:
1 | PrefetchReceiver (来自 L1 的 Hint) + VBOP + PBOP + TP(可选) |
预取请求/训练/响应 Bundle
文件:Prefetcher.scala:112-176
1 | class PrefetchReq extends PrefetchBundle { |
BOP(Best-Offset Prefetch)
文件:coupledL2/prefetch/BestOffsetPrefetch.scala(L2 最大的预取器,约 32KB)
BOP 的思想:在一组候选 offset 中,通过”打分竞赛”找出当前最能命中的 best offset,然后用 预取地址 = 当前地址 + bestOffset 预取
三大组件:
RecentRequestTable (RRT)(最近请求表):记录最近发生的访问地址(去掉块内偏移),用 双哈希异或 索引:
1
2
3
4def hash1(addr) = lineAddr(addr)(rrIdxBits-1, 0)
def hash2(addr) = lineAddr(addr)(2*rrIdxBits-1, rrIdxBits)
def idx(addr) = hash1(addr) ^ hash2(addr) // XOR 折叠
def tag(addr) = lineAddr(addr)(rrTagBits+rrIdxBits-1, rrIdxBits)每项仅
{valid(1b), tag(12b)},用单端口 SRAM(256 项)实现。OffsetScoreTable(offset 打分表):状态机
s_idle → s_learn。每个合格的 L2 访问测试一个候选 offsetd:若(当前地址 - d)在 RRT 中命中,则该 offset 的score++。学习终止条件:某 offset 达到scoreMax(31)或 轮数达到roundMax(50);结束后选出bestOffset作为下一轮prefetchOffset。若bestScore < badScore,则prefetchDisable置位(暂停预取)。DelayQueue(延迟队列,16 项,
dQLatency=175拍):把 miss 地址延迟一段时间再写入 RRT,模拟”这个地址确实经历了一次内存访问延迟”,使打分更符合真实时序。
关键参数(BOPParameters):
| 参数 | VBOP | PBOP |
|---|---|---|
virtualTrain(虚拟地址训练) |
true | false |
rrTableEntries |
256 | 256 |
scoreBits / scoreMax |
5 / 31 | 5 / 31 |
roundMax |
50 | 50 |
badScore |
2 | 1 |
offsetList 规模 |
~96 个 offset | ~32 个 offset(范围更小) |
VBOP vs PBOP:
- VBOP(虚拟地址 BOP):用虚地址训练,生成的预取地址需要经
PrefetchReqBuffer(16 项)发 TLB 请求 翻译成物理地址,跨页时按页边界处理。来源标记Prefetch2L2BOP。 - PBOP(物理地址 BOP):直接用物理地址,无需 TLB,跨物理页则直接丢弃预取。来源标记
Prefetch2L2PBOP。
L2 预取顶层仲裁
文件:Prefetcher.scala:232-426
使能控制(来自核心 CSR PrefetchCtrlFromCore):
1 | val pfRcv_en = RegNextN(pfCtrl.l2_pf_master_en && pfCtrl.l2_pf_recv_en, 2) |
训练分配:BOP/PBOP 不接收 L1 预取触发的训练(避免被 L1 预取行”污染”训练):
1 | vbop.io.train.valid := io.train.valid && (io.train.bits.reqsource =/= L1DataPrefetch.id.U) |
固定优先级仲裁(ParallelPriorityMux,Prefetcher.scala:343-380):
1 | PrefetchReceiver(L1 提示) > VBOP > PBOP > TP |
汇聚后进入 PrefetchQueue(流式队列:永远 ready,满了丢最旧的,保证总是发最新请求),再经 1 拍流水线送往 L2 主流水线。
L2 自身预取请求的下行通路(TileLink Hint)
这是 通路 B 的一个重要分支:L2 的 BOP/TP 等预取器产生的 PrefetchReq,不走旁路,而是在 SinkA 中被转换成 TileLink Hint 操作,进入 L2 主流水线统一处理。
文件:coupledL2/SinkA.scala:79-116
1 | def fromPrefetchReqtoTaskBundle(req: PrefetchReq): TaskBundle = { |
SinkA 仲裁(SinkA.scala:117-131)——外部 demand(A 通道)优先于预取:
1 | io.task.valid := io.a.valid || io.prefetchReq.get.valid |
这条 Hint 在 L2 内部被消费——它驱动 L2 去判断命中/缺失,缺失则向 L3/主存发 Acquire 取数。L2 并不会把这个 Hint 当作”提示 L3 预取”再转发给 L3。
L3 预取器(huancun)
文件:huancun/prefetch/Prefetcher.scala、PrefetchReceiver.scala、BestOffsetPrefetch.scala
L3 预取顶层的三种配置模式
Prefetcher.scala 用 match-case 按参数类型选择实现:
- 纯 BOP(
case bop: BOPParameters):BestOffsetPrefetch → PrefetchQueue → Pipeline → io.req; - L1Hint + BOP(
case receiver: PrefetchReceiverParams):PrefetchReceiver(接收旁路提示,2 拍延迟)与 BOP 并存,接收器优先; - 纯接收器(
case receiver: L3PrefetchReceiverParams):只有PrefetchReceiver,无算法生成。
默认配置是第 3 种——所以默认情况下 L3 不主动生成预取,只执行 L1 经旁路送来的预取地址
L3 的 BOP(默认未启用)
huancun/prefetch/BestOffsetPrefetch.scala 实现了一个与 L2 结构相同、规模更小的 BOP:
- RRT 256 项、单端口 SRAM、双哈希 XOR 索引;
- OffsetScoreTable 36 个 offset、
scoreBits=5、roundMax=50、badScore=1; - 生成预取时做 跨页保护(
getPPN(newAddr) =/= getPPN(oldAddr)则不发); - 来源标记
Prefetch2L2BOP。
它在默认 L3 配置里没有被实例化,属于”可选能力”。
L3 接收端来源信息丢失
如 5.4 所述,L3 的 PrefetchReceiver 把所有来自上层的预取硬编码为 Prefetch2L2Stream(PrefetchReceiver.scala:30,带 TODO: add L3 pfSource),因为旁路 huancun.PrefetchRecv 没有携带 pf_source 字段。这是一个已知的、待 Hint 机制完善后解决的简化。
Temporal Prefetch:经主存的跨层级时间预取
L2 侧的 TP 引擎
文件:coupledL2/prefetch/TemporalPrefetch.scala
参数(TPParameters):tpTableEntries=16384、tpTableAssoc=16(→ nrSet=1024)、triggerQueueDepth=4、tpThrottleCycles(发射节流)。
多阶段流程:
- S0:训练请求查询 L2 本地的
tpMetaTable(记录 trigger tag); - S1:tag 匹配,判定 meta hit/miss,选 victim way;
- S2 决策:
- 若 meta hit(且配置
hitAsTrigger)→ 把该地址作为新 trigger 推入triggerQueue,并发起 读 TPmeta(dataReadQueue); - 若 meta miss 且
triggerQueue非空 → 启动 Recorder,开始记录后续 miss 地址;
- 若 meta hit(且配置
- Recorder:把连续 miss 地址(
>> offsetBits)存入recorder_data,攒够recordThres个后 → 写本地tpMetaTable+ 发起 写 TPmeta(dataWriteQueue); - Sender:从读回的
tpDataQueue取出地址序列,按tpThrottleCycles节流逐个发射预取请求,来源标记Prefetch2L2TP。
与 L3 的接口(TemporalPrefetch.scala,tpmeta_port):
1 | class TPmetaIO(...) extends Bundle { |
L3 侧的 TPmeta 存储
文件:huancun/prefetch/TPmeta.scala、TPmetaParameters.scala
L3 的 TPmeta 模块就是一块 专门存放 TP 元数据的 SRAM:
1 | // TPmetaParameters(DefaultTPmetaParameters) |
读写时序(TPmeta.scala:38-73):
- 写:
wmode=true时按way的 one-hot 写入rawData + hartid; - 读:2 拍延迟返回;响应需校验
hartid匹配,保证多核数据隔离; shouldReset=false:复位时 不清零,让学习到的访问模式可以跨程序启动保留。
TP 跨层连接(SoC 顶层)
文件:Top.scala:185-196
1 | l3.tpmeta_recv_node.foreach(recv => { |
完整 TP 数据流:
1 | L2 TP Recorder ──TPmetaReq(write)──▶ core_l3_tpmeta_source_port ──▶ l3.tpmeta_recv_node |
这里 L3 充当的是 TP 的”远端大容量元数据存储”,而不是预取算法本身。TP 的”大脑”在 L2,”记忆库”在 L3
FDP:反馈式预取调节
文件:FDP.scala(Feedback Directed Prefetching)
FDP 通过运行时统计 预取准确率、时效性、污染率,动态调节 L1 预取的激进程度(dynamic_depth),形成闭环。
三个辅助结构:
CounterFilter(
FDP.scala:61-150):FIFO(大小 ≈ 4 级流水 × Load 单元数),去除 Load 流水线中对 同一 cache line 的重复 prefetch-hit 计数。BloomFilter(
FDP.scala:170-206):布隆过滤器,快速判断一个预取地址 是否已在途(MSHR)或已预取,避免重复发射。哈希方式是把块物理地址高低两段 XOR:1
def get_addr(paddr) = { val b = paddr(.., blockOffBits); b(low) ^ b(high) }
接口:
set(发射时置位)/clr(填充完成时清除)/query(新请求查询)FDPrefetcherMonitor(
FDP.scala:228-303):每INTERVAL=8192周期统计一批计数器:计数器 含义 total_prefetch总预取数 useful_prefetch有用预取(被 demand 命中)数 late_prefetch晚到预取数 demand_miss需求 miss 数 pollution污染(因预取导致的 miss)数 据此计算 准确率 = useful/total、时效性 = late/total、污染率 = pollution/demand_miss(
XSPerfRolling),输出调节信号:1
2
3
4
5
6class PrefetchControlBundle extends XSBundle {
val enable = Bool() // 总开关
val dynamic_depth = UInt(6.W) // 动态预取深度 → 喂给 Stream 的 dynamic_depth
val confidence = UInt(1.W) // 是否允许覆盖 Load 端口
val flush = Bool() // 清空预取状态
}
一条预取请求的完整生命周期
把前面所有环节串起来,以下是六条预取路径的对比。
路径 1:L1 自取(最快,sink = SINK_L1)
1 | Load → L1Prefetcher(Stream/Stride) 训练 → 生成 sink=SINK_L1 的请求 |
- 内部来源标记
L1_HW_PREFETCH_STREAM/STRIDE;有反压;零跨层延迟。
路径 2:L1 让 L2 取(通路 A 旁路,最常见的跨层)
1 | L1Prefetcher(Stream/Stride) 或 SMS → io.l2_req (ValidIO) |
- 来源:
Prefetch2L2SMS/Stream/Stride(10/11/12);无 TLB(地址已是物理);无反压。
路径 3:L2 自取(通路 B,L2 内部)
1 | L2 BOP/PBOP/TP 训练(基于 L2 的 miss/prefetch-hit) → PrefetchReq(pfSource=BOP/PBOP/TP) |
- BOP/PBOP 的
needAck=true;BOP/PBOP 不接受 L1 预取触发的训练。
路径 4:L1 让 L3 取(通路 A 旁路,跨两级)
1 | L1Prefetcher(仅 Stream, L3_DEPTH_RATIO=3) → io.l3_req (ValidIO) |
- SMS 不走这条;到 L3 来源信息丢失(TODO)。
路径 5:L3 自取(默认未启用)
1 | (仅当 L3 配置 BOPParameters 时)L3 BOP 训练 → PrefetchReq → L3 主流水线 → 填入 L3 |
- 默认 L3 只配
L3PrefetchReceiverParams,此路径默认关闭。
路径 6:TP 经主存(通路 C,跨层最远)
1 | L2 TP Recorder 记录 miss 序列 → TPmetaReq(write) → core_l3_tpmeta_source_port |