跳转至

昆明湖 BPU 模块 gem5 文档

文档说明及背景知识

在昆明湖架构的前端中,分支预测单元(BPU)的 gem5 模型最为详尽,最接近 RTL 的实现。本文档旨在帮助读者理解 BPU 的 gem5 模型,允许读者进一步得对其进行修改。

我们建议读者在阅读后续内容前先了解前端 BPU 的架构,关注以下内容即可:

  • Reinman G, Austin T, Calder B. A scalable front-end architecture for fast instruction delivery[J]. ACM SIGARCH Computer Architecture News, 1999, 27(2): 234-245.

    本篇论文介绍了解耦式前端的基本工作原理,与昆明湖架构极为相关,阅读第 1,3 section 来熟悉 FTB,FTQ 这些组成部分的职责和他们的交互方式。

  • xiangshan-design-doc/docs/frontend/BPU/index.md

    昆明湖的前端架构延续了上述论文的思路,但也存在不同之处。阅读此文档,有助于读者构建对昆明湖前端架构的全局了解。我们的后续讨论也将基于昆明湖架构,这要求读者适应它的设计文档的术语,以及这些术语在昆明湖架构下的具体意义(很可能和其他论文存在出入)。

    虽然xiangshan-design-doc为每一个BPU的子模块都提供了详细的介绍,只希望从全局的视角理解BPU的读者只阅读index.md就足够了,读者可以将子模块视为黑箱,知道各子模块的职责和接口即可。

BPU 源代码的文件结构

熟悉了昆明湖的 BPU 架构后,我们就可以来尝试理解它在 gem5 模拟器里的实现。

首先,让我们看一下 BPU 的文件结构:

src/cpu/pred/
├── btb/
│   ├── btb.cc
│   ├── btb.hh
│   ├── btb_ittage.cc
│   ├── btb_ittage.hh
│   ├── btb_sc.cc
│   ├── btb_sc.hh
│   ├── btb_tage.cc
│   ├── btb_tage.hh
│   ├── btb_ubtb.cc
│   ├── btb_ubtb.hh
│   ├── decoupled_bpred.cc
│   ├── decoupled_bpred.hh
│   ├── fetch_target_queue.cc
│   ├── fetch_target_queue.hh
│   ├── folded_hist.cc
│   ├── folded_hist.hh
│   ├── history_manager.hh
│   ├── jump_ahead_predictor.hh
│   ├── loop_buffer.hh
│   ├── loop_predictor.hh
│   ├── ras.cc
│   ├── ras.hh
│   ├── stream_struct.hh
│   ├── test/
│   ├── timed_base_pred.cc
│   ├── timed_base_pred.hh
│   ├── uras.cc
│   └── uras.hh
├── ftb/
├── stream/
├── BranchPredictor.py
├── README.md
├── SConscript
...
BPU 的全部源代码都放在 src/cpu/pred/中,读者会发现这个文件夹里还有很多其它文件被省略掉了,(比如 src/cpu/pred/ras.hh),这是因为香山的gem5模型是基于官方gem5开发的。而官方gem5的代码库里本来就提供一些常见微架构的模拟代码(gem5 stdlib?)。我们在香山的 gem5 代码库中,保留了这些文件,但是实际使用的则是以上列举的文件。可以看到真正使用的文件大部分都在 src/cpu/pred/ftb/中。

其中 decoupled_bpred.cc 是 BPU 的顶层文件,它提供接口供其他组件使用(比如前端的顶层文件(fetch.cc?)),同时它也包含了 BPU 的运行逻辑,并在内部调用各个子模块的接口来完成 BPU 的功能。

每一个子模块都在自己的文件中,例如:

  1. btb.cc:BTB 的实现
  2. btb_tage.cc:TAGE 的实现
  3. ras.cc:RAS 的实现

最后,我们在 stream_struct.hh 中定义了一些公共的结构体,比如 Fetch Block,FullPrediction 等。

BPU 顶层文件:decoupled_bpred.cc/hh

下面将介绍 BPU 运行过程中较为关键的几个流程,或者涉及 BPU 的流程,希望通过这些流程,读者能更直观的理解 BPU 的接口的意义和用法。

更多关于 BPU 和 Fetch 交互的细节,请参见BPU 与 Fetch 交互

Fetch::tick() 访问 FTQ

Fetch::tick() 是整个前端的顶层函数,而其中的 fetch() 函数负责模拟 IFU 的运行,目前我们没有模拟 IFU 的微架构,而是在一个 tick 内,根据 FTQ 项,获取整条 Fetch Block 的指令码,并人工地增加延迟。以下是 Fetch::fetch() 中访问 FTQ 的流程:

  • 通过主循环遍历当前 Fetch Block 中的指令。
  • 在每条指令的处理过程中,调用 BPU::decoupledpredict() 接口,访问 FTQ,获取 next_pc。
  • BPU::decoupledpredict() 可以被视作 FTQ 的一个 access helper function,了解微架构的读者可能会发现模型和 RTL 在这里的差异:前者需要在 fetch 每一条指令时都访问该指令所在的 FTQ 项,而后者则是一次性的收到一个 FTQ 项作为流水线的输入。

下面提供一段 inline 过的伪代码,只对 BPU 感兴趣的读者可以重点关注 BPU 和 FTQ 的接口的调用:

// IFU的顶层函数
Fetch::tick()
|  // helper function
|--Fetch::fetch()
|  |  // 主循环,每个迭代对应一条指令的fetch
|  |--while( numInst < fetchWidth && ! predictBranch)
|  |  |
|  |  |  // helper function
|  |  |--predictBranch |= Fetch::lookupAndUpdateNextPC( next_pc)
|  |  |  |  // 调用BPU的接口,获取next_pc和跳转与否
|  |  |  |--BPU::decoupledpredict(&pc)
|  |  |     |  // 调用FTQ的接口,获取当前正在读取的FTQ项(对应RTL的ifuPtr)
|  |  |     |--FTQ::getTarget()
|  |  |     |--set(pc, target)
|  |  |     |--if(run_out_of_this_entry)
|  |  |     |  FTQ::processFetchTargetCompletion()
|  |  |--set(this_pc, *next_pc)
|--BPU::trySupplyFetchWithTarget()

相关内容链接: - BPU 与 fetch 的接口

FTQ 转发 redirect 信号以及对 BPU 子预测器的更新

我们的微结构中,FTQ 会将来自后端或 IFU 的 redirect 信号转发给 BPU,同样的,当 FTQ 收到来自 commit stage 的信号,得知 fetch block 被全部提交后,也会用它存储的 meta 信息更新 BPU 的子预测器。我们的 gem5 模型中也做了等效的处理,只不过我们把这两种逻辑放在 BPU::controlSquash() 和 BPU::update() 中。而它们的调用是发生在 Fetch::checkSignalAndUpdate() 中,下面是伪代码:

Fetch::tick()
|
|--Fetch::checkSignalAndUpdate()
   |
   |--if(fromCommit->commitInfo[tid].squash)
   |  BPU::controlSquash()
   |--BPU::update()

分支预测流水线

BPU::tick() 是 BPU 的顶层函数,它负责模拟分支预测流水线,也就是从 S0 pc 生成三个 stage 的预测结果,再将最准确的结果推到 FTQ 中的过程。BPU 在模型上的原理和微架构类似:

[UBTB] ->   [BTB/TAGE/ITTAGE]
   ↓         ↓           ↓
   └──→ [FetchStream Queue] 
     [FetchTarget Queue]
     [Instruction Cache]

每个预测器都会每拍生成预测结果,其中 BTB/uBTB 生成最核心的 BTBEntry, 然后其他预测器按需填入对应的方向或者别的信息, 共同生成每一级的 FullBTBPrediction(predsOfEachStage), 最后 3 选 1 得到最终的 FullBTBPrediction(finalPred);

下一拍会根据 finalPred 结果生成一项 FSQEntry 放入 FSQ 中

再下一拍会用 FSQEntry 生成一个 FTQEntry 放入 FTQ 中

这里需要注意模型对微架构的一个简化:

在微架构中,对于某一个 s0 pc,它的 S1 到 S3 的预测结果是在三拍中相继生成的,而我们的模型在获得 s0 pc 后的第一拍就生成了 S1 到 S3 的预测结果。如果我们假设没有 override*发生, 那么这个简化与微架构是等效的。但是细心的读者会发现:在有 override 的情况下,微架构会在产生 override 的那拍重定向 s0 pc,并覆写已经写入的 FTQ 项。也就是说最终的预测结果在发生 override 的那拍才会写入 FTQ。这样一来我们的模型就与微架构不一致了。为了弥补这个差异,我们为有 override 的 s0 pc 手动加入相应的延迟。具体做法是通过变量 numOverrideBubbles。

*:override:一旦高级预测器在后续流水级的预测结果与已有结果不一致,就将会使用高级预测器结果作为新的输出更新后续 FTQ 中存储预测块结果 并重定向 s0 级 PC,清空新结果流水级之前的流水级的错误路径结果。

BPU::tick()
|
|--if(!receivedPred && numOverrideBubbles == 0)
|  // 生成最终预测结果,并创建override bubbles
|  BPU::generateFinalPredAndCreateBubbles()
|  |
|  |--finalPred = predsOfEachStage[numStages - 1]
|  |--numOverrideBubbles = firstHitStage
|  |--receivedPred = true
|
|--processEnqueueAndBubbles()
|  |
|  |--tryEnqFetchTarget();
|  |  |
|  |  |--if (!validateFTQEnqueue())
|  |  |  return;
|  |  |--ftq_entry = createFtqEntryFromStream(ftq_enq_state.streamId)
|  |  |--fetchTargetQueue.enqueue(ftq_entry);
|  |
|  |--tryEnqFetchStream();
|  |  |
|  |  |--if (!validateFSQEnqueue())
|  |  |  return;
|  |  |--processNewPrediction(true);
|  |  |  |
|  |  |  |--entry = createFetchStreamEntry();
|  |  |  |--s0PC = finalPred.getTarget(predictWidth);
|  |  |  |--updateHistoryForPrediction(entry);
|  |  |  |--fetchStreamQueue.emplace(fsqId, entry);
|  |  |  |--fsqId++;
|  |  |
|  |  |--receivedPred = false;
|  |  
|  |--if (numOverrideBubbles > 0)
|     numOverrideBubbles--;
|
|--requestNewPrediction()
   |
   |--if (!receivedPred)
      |--for (int i = 0; i < numComponents; i++)
         components[i]->putPCHistory(s0PC, s0History, predsOfEachStage);

正在修订的内容

模型运行过程中,一个 fetch block 的生命周期
其他模型的技巧:FSQ+FTQ = RTL 的 FTQ
数据结构的关系(BTBEntry vs FullBTBPrediction)
关键函数解释(参见 cpu/pred/README.md)
特殊功能
变量解释
子预测器的抽象