BLOG

Record, summarize, and improve.

Minor&O3

Minor“有限的投机执行” Minor CPU 的投机执行是如何工作的? Minor 与 O3 CPU 在投机执行上的本质区别 总结 Minor顺序发射乱序执行 1. 前端投机(取指与译码):与 Minor 保持一致 2. 后端投机(执行与写回):取决于“写回”的实现路线 路线 A:无 ROB 模式(纯顺序发射、乱序完成 / Scoreboard 模式) 路线 B:带 ROB 模式(顺序发射、乱序执行、顺序提交 / Commit) 3. 三种架构投机执行全面对比 4. 这对你排查向量指令(Vector)数据有什么启示? 一、 可以乱序的阶段(核心执行区) 1. 发射阶段 (Issue) —— 可以乱序 2. 执行阶段 (Execute) —— 可以乱序 3. 写回阶段 (Writeback) —— 可以乱序 二、 必须严格顺序的阶段(不可乱序) 1. 前端:取指 (Fetch) 与 译码 (Decode) —— 严格顺序 2. 中端:寄存器重命名 (Rename) 与 分配 (Allocate) —— 严格顺序 3. 后端:提交 / 退休 (Commit / Retire) —— 严格顺序 三、 总结快查表 体系结构演进 第一阶段:单周期与多周期经典架构(早期的冯·诺依曼) 第二阶段:流水线架构(Pipelined RISC) 第三阶段:顺序超标量架构(In-Order Superscalar) 第四阶段:乱序执行超标量架构(Out-of-Order Execution, OoO) 第五阶段:多核与多线程架构(Multi-Core & TLP) 第六阶段:领域特定架构与异构加速(DSA / Heterogeneous) 总结:架构演进的底层逻辑

Minor“有限的投机执行”

gem5 中的 Minor CPU 虽然是一个严格的顺序(In-Order)流水线模型,但它支持基于分支预测的投机执行(Speculative Execution)。这意味着它会预测分支方向并提前取指、译码,但在指令实际执行和内存访问的阶段,它的投机行为受到严格限制,与 O3(乱序)CPU 有着本质的区别。

Minor CPU 的投机执行是如何工作的?

Minor CPU 拥有一个固定的 4 级流水线:Fetch1 $\rightarrow$ Fetch2 $\rightarrow$ Decode $\rightarrow$ Execute。它的投机主要发生在这个流水线的推进过程中:

  1. 投机取指与预测(Fetch1 & Fetch2)

    在流水线的前端,Minor CPU 会调用配置的分支预测器(Branch Predictor)。如果遇到条件跳转或间接跳转指令,它不会停下来等待结果,而是根据预测的方向,直接投机性地从内存/Cache 中读取(Fetch1)并分解出(Fetch2)后续的指令。

  2. 流水线推进(Decode)

    这些投机获取的指令会像正常指令一样,继续流向 Decode 阶段(译码成微码 micro-ops)并送往 Execute 阶段的输入队列中。

  3. 误判冲刷(Squash)

    当那条引发预测的分支指令在 Execute 阶段真正执行、计算出真正的跳转结果时:

    • 如果预测正确,投机成功,流水线无缝继续运行。
    • 如果预测失败(Misprediction)Execute 阶段会立刻向前端发送一个 Squash(冲刷)信号。这会彻底清空目前停留在 Fetch1、Fetch2、Decode 以及 Execute 队列中所有处于“投机路径”上的年轻指令,并将 PC 指针重定向到正确的路径上重新取指。

因此,你在 gem5 的 Minor CPU 日志中,经常能看到诸如 SquashingResetting PC 的行为,这正是前端投机执行失败后,硬件模型在进行清理和纠错。

Minor 与 O3 CPU 在投机执行上的本质区别

虽然 Minor 会投机地把指令“装填”进流水线,但为了保持顺序执行的简单性,它在执行(Execute)和访存(Memory Access)阶段筑起了防线:

特性 Minor CPU (In-Order) O3 CPU (Out-of-Order)
前端投机(取指/译码) 支持(基于分支预测,消除前端气泡) 完全支持(更加激进,通常带有更深的前端队列)
后端投机(算术执行) 不支持乱序投机。指令必须按程序顺序(In-Order)执行。如果前面有指令被阻塞(如等待慢速访存),后面的投机指令也会在 Execute 阶段被堵住。 完全支持。只要操作数就绪,哪怕分支结果还没出来,后面的独立指令也可以跨过前面被 Stall 的指令提前投机执行。
投机访存(Load/Store) 极其保守。虽然它有 LSQ(Load/Store Queue),但访存请求通常必须在确定自己不是假路径(即不会被前面的分支 Squash)时,才真正向 Cache 发出请求。 完全支持投机访存。支持激进的 Store-to-Load Forwarding(内存伪相关预测),甚至能在分支结果没出来前就去读写 Cache。
投机状态保存恢复 结构非常简单。靠直接冲刷(Squash)物理流水线寄存器。没有 ROB(重排序缓存)。 结构非常复杂。依靠 ROB (Reorder Buffer) 和 RAT(寄存器别名表)来严格区分投机状态与正式状态,只有确定不投机的指令才能安全 Commit。
总结

一句话概括:gem5 的 Minor CPU 允许“投机地把指令取出来并译码”,但“不允许指令跨越阻塞去投机执行,也不允许投机地访问内存”。 它的投机主要是为了消除分支指令带来的前端气泡(Bubble),而不是为了压榨指令级并行度(ILP)。这也是为什么在 Minor 的日志里你依然会看到指令被发射(Issue),但随后由于前方阻塞或分支预测失败,它们被直接 Squash 掉而无法完成(Commit)。

Minor顺序发射乱序执行

如果你将 Minor CPU 改造成“顺序发射(In-Order Issue)、乱序执行(Out-of-Order Execution)”的混合架构,这在计算机体系结构中是一个非常经典的演进阶段。这种设计(类似于早期的 Scoreboard/记分板 架构,或者近代 ARM Cortex-A53/A55 处理某些多周期指令的逻辑)主要是为了解决长周期指令(如浮点、向量、Mem Access)阻塞后续短周期整数指令的问题。

在这种混合架构下,投机执行(Speculation)的行为会发生显著的分化,具体取决于你是否为后端引入了 ROB(Reorder Buffer,重排序缓存)

1. 前端投机(取指与译码):与 Minor 保持一致

在流水线的前端(Fetch $\rightarrow$ Decode),它的投机行为和修改前的 Minor CPU 没有区别。

  • 依然完全依赖分支预测器。
  • 遇到分支指令时,前端会盲目地沿着预测方向把指令一条条取出来、译码,并按程序顺序(In-Order)送入发射队列(Issue Queue)。
2. 后端投机(执行与写回):取决于“写回”的实现路线

在指令离开准备发射的阶段后,如何处理“分支指令还没算出结果,但后面的指令已经乱序执行完”的情况?这有两种截然不同的路线:

路线 A:无 ROB 模式(纯顺序发射、乱序完成 / Scoreboard 模式)

为了保证硬件的极简,如果不添加 ROB,指令在执行完后会直接修改物理寄存器堆(Register File)。

  • 后端投机能力:几乎为零(极度保守)。
  • 处理逻辑:因为指令是乱序执行的,如果允许发射队列跨过“尚未解决的分支指令”去执行后面的指令,一旦分支预测失败,后面已经执行完并修改了寄存器状态的指令就无法撤销了(这会导致 Precise Exception 遭到破坏)。
  • 结果:在这种模式下,所有被预测的分支指令都会成为后端的“断流阀”。在当前的条件分支指令没有在流水线中执行完毕、确定对错之前,发射队列中它后面的所有投机指令绝对不允许开始执行。此时的乱序执行,仅仅用来解决“向量/浮点指令未完,后面的独立整数指令先走”的非投机重叠。
路线 B:带 ROB 模式(顺序发射、乱序执行、顺序提交 / Commit)

如果你在 Minor 的后端增加了 ROB,虽然发射是顺序的,但指令执行完后并不直接修改架构寄存器,而是将结果扔进 ROB 中排队。

  • 后端投机能力:大幅解放(接近 O3 CPU)。
  • 处理逻辑:发射队列可以非常激进。即使前面的分支指令还在功能单元里算得大汗淋漓,只要后面的指令没有数据依赖(RAW),发射队列就可以把它们顺序地射出去、乱序地在功能单元里执行完
  • 结果:如果分支预测正确,ROB 按顺序正式提交(Commit)修改寄存器;如果分支预测失败,直接根据 ROB 的指针一刀切,把分支之后的投机执行结果全部原地擦除(Flush)。
3. 三种架构投机执行全面对比

将这种“混合架构”放到原本的 Minor 和 O3 之间,它们的投机能力光谱如下:

特性维度 Minor CPU (纯顺序) 混合架构 (顺序发射/乱序执行) O3 CPU (完全乱序)
前端投机 支持(基于分支预测取指) 支持(基于分支预测取指) 支持(极激进的大吞吐取指)
后端算术投机 不支持。前面堵住,后面无论如何不能动。 部分支持(若有ROB):可以跨越未解决的分支提前执行算术。 完美支持:完全解耦,只要操作数就绪即可投机执行。
投机访存 (Mem) 不支持。严格按顺序发送给 Memory 系统。 极度保守。由于没有乱序发射带来的内存依赖检查(如 Memory Disambiguation),通常无法投机读取 Cache。 完全支持:支持激进的 Load/Store 投机和内存伪相关预测。
分支预测失败代价 较低(只冲刷浅层物理流水线寄存器)。 中等(取决于投机执行了多少,带 ROB 时冲刷 ROB 成本稍高)。 极高(因为乱序执行得太深,冲刷会浪费大量已经被执行的无效功)。
4. 这对你排查向量指令(Vector)数据有什么启示?

回到你上一问中看到的 Trace 比对文件:

  1. 结构冲突(Structural Hazard)的体现会改变

    如果改成这种混合架构,你会发现当多条 vle16 向量访存指令在前面卡住时,后面原本在 Minor 中会被一同卡死的 addi 等整数指令,现在其延迟(Latency)会显著缩小,因为整数指令可以乱序越过向量单元先执行完。

  2. 写回端口堵塞(Writeback Conflict)会暴增

    如果采用“路线 A(无 ROB)”,由于指令乱序完成,一条需要 15 周期执行完的向量指令可能刚好和一条只需要 1 周期执行完的整数指令在同一个周期内尝试写回寄存器堆。这在硬件上会产生严重的写端口冲突。如果在 UVM 侧观察到由于端口冲突导致频繁的 Stall,而 gem5 模型的写端口是完美的(无限端口),那么在密集向量流中,两边的 Latency Gap 依然会拉大。

在一个经典的乱序执行(OoO)处理器流水线中,并不是所有地方都能“乱来”的。为了保证程序运行的逻辑正确性,现代 CPU 遵循一个极其核心的铁律:“顺序取指/译码、乱序发射/执行/写回、顺序提交”(In-Order Fetch/Decode, Out-of-Order Issue/Execute/Writeback, In-Order Commit)。

我们可以将流水线划分为三个大的政治阵营:前端(绝对顺序)核心执行区(可以乱序)和后端(绝对顺序)

一、 可以乱序的阶段(核心执行区)

当指令通过了前端的检查,进入到发射队列(Issue Queue / Reservation Stations)之后,就正式进入了可以乱序的“自由世界”。

1. 发射阶段 (Issue) —— 可以乱序
  • 怎么乱:指令进入发射队列后,不再看它在程序里的先后顺序,而是看它的操作数(Operands)是否就绪
  • 举个例子:程序里先写了 指令A (等内存),后写了 指令B (加法)。只要 指令B 的两个加数都准备好了,它就可以超越还在死等内存的 指令A,被优先发射到加法器里去。
2. 执行阶段 (Execute) —— 可以乱序
  • 怎么乱:不同的功能单元(FU)执行时间天差地别。
  • 举个例子指令B(加法) 只需要 1 个周期,指令C(乘法) 需要 4 个周期。哪怕它们同一个周期开始执行,指令B 也会比 指令C 更早执行完。各单元各自为政,执行完成的顺序完全是乱的。
3. 写回阶段 (Writeback) —— 可以乱序
  • 怎么乱:指令执行完后,需要把结果写回到物理寄存器堆(PRF)或者 ROB(重排序缓存)的临时槽位里。因为执行是乱序完工的,所以写回也是谁先做完谁先写。
  • 注意:这里的写回并不是修改程序员能看到的那个寄存器(架构寄存器),而是写到了“草稿纸”(临时状态)上。

二、 必须严格顺序的阶段(不可乱序)

为了在乱序执行的狂欢中保持理智,流水线的前端和末端筑起了钢铁防线,必须严格按程序原本的顺序(In-Order)推进。

1. 前端:取指 (Fetch) 与 译码 (Decode) —— 严格顺序
  • 原因:CPU 必须顺着程序计数器(PC)的指引(或者分支预测的方向)一行行读代码。你连后面有什么指令、指令是在干嘛都不知道,根本无从谈起“乱序”。
2. 中端:寄存器重命名 (Rename) 与 分配 (Allocate) —— 严格顺序
  • 原因:这是进入乱序区的闸口。CPU 必须在这里按顺序理清指令之间的数据依赖关系(谁在用谁的结果),并将架构寄存器映射到物理寄存器上。如果把这个顺序搞乱了,原本的“先读后写(WAR)”或“写后写(WAW)”的依赖关系就会彻底崩溃。
3. 后端:提交 / 退休 (Commit / Retire) —— 严格顺序
  • 原因:这是保证 CPU 算不垮的最重要阶段。所有的乱序执行结果都只是“临时投机”的。只有当一条指令前面所有的指令都安全、正确地执行完(没有报错、没有分支预测失败),这条指令才能在 ROB(重排序缓存) 的头部按程序顺序正式提交,从而永久性地修改架构寄存器和内存。
  • 为什么必须顺序:为了实现精确异常处理(Precise Exception)。如果在 指令A(除以0报错) 还没处理时,排在后面的 指令B(乱序执行完了) 就提前把结果写入了内存,那程序崩溃时就无法实现完美回滚,整个计算机系统就会陷入混乱。

三、 总结快查表

流水线阶段 英文名称 是否可乱序 核心原因 / 硬件机制支撑
取指 Fetch ❌ 必须顺序 必须跟随 PC 或分支预测的程序流向
译码 Decode ❌ 必须顺序 硬件需要按顺序解析指令和微码
重命名/分配 Rename / Alloc ❌ 必须顺序 必须按程序逻辑建立正确的数据依赖链
发射 Issue ** can 乱序** 依靠发射队列(IQ/RS),操作数就绪即可发射
执行 Execute ** can 乱序** 依靠多功能单元(ALU/FPU/MEM),长短周期指令并存
写回 Writeback ** can 乱序** 结果先写回物理寄存器或 ROB 暂存
提交 Commit / Retire ❌ 必须顺序 依靠重排序缓存(ROB),确保精确异常与逻辑正确

体系结构演进

计算机体系结构的演进是一部“如何榨干晶体管性能”“如何向物理极限妥协”的交响乐。从最早期的串行计算到如今的异构加速,每一次演进都是为了解决当时的性能瓶颈(如功耗墙、内存墙)。

以下是计算机体系结构演进的六大核心阶段,以及它们各自的优缺点:

第一阶段:单周期与多周期经典架构(早期的冯·诺依曼)

在芯片诞生的初期,硬件资源极其珍贵。程序是完全串行执行的。

  • 单周期(Single-Cycle):一条指令在一个极长的时钟周期内完成所有工作(取指、译码、执行、访存、写回)。
  • 多周期(Multi-Cycle):将指令拆分成多个短周期,简单的指令(如 ADD)用 3 周期,复杂的(如 LW)用 5 周期。
优点 缺点
结构极简:控制逻辑极其简单,不需要处理任何复杂的指令间冲突。 性能极低:单周期受限于最慢的指令(通常是访存),主频根本拉不高;多周期的吞吐量(Throughput)也无法产生重叠。
第二阶段:流水线架构(Pipelined RISC)

受到工业流水线装配汽车的启发,体系结构引入了流水线(Pipeline)概念。最经典的就是 MIPS 5 级流水线(IF, ID, EX, MEM, WB)。它让多条指令在空间上重叠执行,实现了“这一条在写回,下一条在访存,再下一条在执行”的宏观并行。

优点 缺点
理想吞吐量大:在理想状态下,每个时钟周期都能吐出一条指令,主频大幅提升。 三大冒险(Hazards):严重受限于结构、数据和控制冲突。分支预测失败或数据未就绪时,流水线会频繁产生“气泡(Bubble)”导致卡顿。
第三阶段:顺序超标量架构(In-Order Superscalar)

单流水线的极限是每个周期发射一条指令($CPI \ge 1$)。为了打破这个限制,超标量架构应运而生——直接在芯片上并排铺设多条流水线,一个周期同时发射(Issue)多条指令。此时指令依然是严格按照程序里的先后顺序进入流水线的。

优点 缺点
指令级并行(ILP)初现:理想情况下,一个周期可以执行 2 到 4 条指令,突破了单流水线的性能天花板。 极度依赖编译器:由于是顺序发射,一旦前面的指令因为等待内存卡住,后面并排的其他流水线全部都要跟着一起空转(Stall),非常容易“一人卡壳,全家瘫痪”。
第四阶段:乱序执行超标量架构(Out-of-Order Execution, OoO)

为了彻底解决顺序执行被慢指令卡死的问题,现代高性能 CPU(如 Intel Core、AMD Ryzen、Apple M 系列、gem5 中的 O3 CPU)全面转向了乱序执行。

它引入了发射队列(Issue Queue)重排序缓存(ROB)和寄存器重命名(Register Renaming)技术。只要指令的数据操作数准备好了,哪怕它在程序里排在后面,CPU 也可以跨越前面被卡住的指令提前投机执行,最后再通过 ROB 顺序提交(Commit)以保证结果正确。

优点 缺点
压榨性能的巅峰:极大程度地挖掘了硬件的指令级并行度(ILP),能极其完美地隐蔽慢速内存带来的延迟。 功耗墙与复杂度爆炸:硬件逻辑极其恐怖。用于检查依赖和保存中间状态的结构占用了大量芯片面积,功耗(Power)和发热量随着乱序深度的增加呈非线性暴增。
第五阶段:多核与多线程架构(Multi-Core & TLP)

大约在 2005 年左右,单核的乱序执行和主频提升触碰到了“功耗墙(Power Wall)”——再提升主频,芯片就要烧毁了。体系结构被迫从挖掘“单程序内部的并行(ILP)”转向挖掘“多任务/多线程之间的并行(TLP)”。不再把单核做大,而是在一块芯片上放多个相对精简的核。

优点 缺点
绕过功耗墙:用较低的主频和多个核心,换取了总算力的成倍提升,能效比更高。 编程难度地狱级:受限于阿姆达尔定律(Amdahl's Law)。如果软件中无法被并行的串行部分占比为 $r$,那么无论堆多少个核,加速比上限都只有 $\frac{1}{r}$。此外还带来了复杂的**缓存一致性(Cache Coherence)**问题。
第六阶段:领域特定架构与异构加速(DSA / Heterogeneous)

进入后摩尔时代(Post-Moore's Law),通用 CPU 的性能红利几乎消耗殆尽,但 AI、大数据、图像处理的需求却呈指数级爆发。于是演进到了当下的最前沿:让专业的硬件干专业的事。不再指望 CPU 包办一切,而是通用 CPU 充当“大管家”,将特定任务甩给 GPU(图形与通用并行)、TPU/NPU(神经网络)、DSA(专用领域架构)或专用向量加速器(Vector Accelerator)。

优点 缺点
极致的能效与吞吐量:针对特定算法(如矩阵乘法、向量加法),DSA 相比通用 CPU 可以实现几个数量级(100x~1000x)的性能和能效提升。 缺乏灵活性(Flexibility):硬件彻底固化。一旦算法发生重大重构(例如从 CNN 变到 Transformer,或者引入了新的稀疏算子),原本昂贵的 DSA 芯片可能会直接变成“废铁”。
总结:架构演进的底层逻辑

从历史来看,计算机体系结构的演进其实是在两个极端之间不断寻找平衡点:

  • 一端是“极致的通用与动态性”:以高功耗、复杂硬件(如 OoO CPU)为代价,试图让任何写得烂的垃圾代码都能跑得飞快。
  • 另一端是“极致的专用与静态性”:以牺牲通用性为代价,依靠强大的编译器或固化的硬件(如 Vector、DSA),在特定领域榨干每毫瓦电力的性能。

你在前几个问题中比对的 gem5 Minor CPU、O3 CPU 以及带有 Vector 扩展的模型,刚好就落在这个演进光谱的第四、五、六阶段交界处。

你在当前的架构设计或仿真排查中,主要关注的是偏向传统的多核/乱序大核(如高性能服务器 CPU),还是偏向软硬件协同的异构加速/DSA(如 AI/视觉芯片)呢?