资讯动态

超标量处理器全面解析:从IPC到乱序执行与寄存器重命名

发布时间:2026/9/15 20:21:12 来源:尧图企业网站定制
这篇是体系结构学习笔记系列的第六篇聊超标量处理器Superscalar Processor。学到这里先要有心理准备它和前面几篇的流水线、冒险这些基础章节不一样超标量把流水线“时间重叠”的思路升级成了“空间并行”的思路CPU里几乎所有关键部件——取指、译码、发射、执行、访存、提交——都得为了“每个周期多发几条指令”这个目标重新设计一遍。虽然有点烧脑子但搞明白它之后再看任何现代CPU的微架构资料你会觉得世界一下子清晰了很多。这篇文章不会堆一堆论文式的术语我会从“它到底解决了什么问题”开始把超标量为什么会出现、流水线各阶段怎么改造、乱序执行背后的寄存器重命名和Tomasulo算法、分支预测为什么变成老大难以及它跟VLIW、多核这些技术路线的关系都过一遍最后聊聊我自己学习时踩过的坑和排查思路。适合刚啃完流水线、准备进入指令级并行ILP这部分的朋友。1. 为什么说IPC能超过1才是超标量1.1 IPC等于1已经是天花板这句话到底什么意思先回顾一个基础概念。单周期处理器一个时钟周期执行一条指令CPICycles Per Instruction等于1。流水线处理器把指令执行拆成取指、译码、执行、访存、写回等多个阶段让不同指令的不同阶段重叠起来理想状态下CPI可以逼近1也就是一个周期完成一条指令。但请注意流水线只是把一个车道优化到了极致它始终只有一条车道。每个周期最多只能取一条指令、只能发射一条指令、只能写回一条指令。就算后面的站台空着前面来的车也只有一辆。所以标量流水线再怎么优化CPI的下限就是1IPCInstructions Per Cycle的上限就是1。超标量做的事情就是把这个“一条车道”拓宽成“多条车道”。每个周期可以同时取多条指令、同时发射多条指令、同时执行多条指令让IPC真正超过1。Intel Pentium是双发射IBM POWER、MIPS R10000、Alpha 21264、ARM Cortex-A系列、AMD Zen系列这些都是超标量。可以说从20世纪90年代开始高性能处理器的地基就是超标量。这里可以用厨房来类比。单周期CPU就像一个厨师独自做一整桌菜流水线是后厨分工——一个切菜、一个炒菜、一个摆盘出菜速度从“做完一道再下一道”变成“连续出菜”但同一时刻灶台上只能有一道菜在炒。超标量就是加了两个灶台同一时间能开好几口锅出菜吞吐量自然上去了。1.2 并行从哪来指令级并行ILP的本质问题接着来了CPU凭什么能每个周期同时发射多条指令因为程序里相邻的指令很多时候互相不依赖天然就可以并行执行。比如这段伪代码LD x1, 0(x2) # 第1条加载 ADD x3, x1, x4 # 第2条依赖x1必须等第1条完成 MUL x5, x6, x7 # 第3条完全独立 SUB x8, x9, x10 # 第4条完全独立第3条和第4条既不需要等第1条也不影响第2条它们完全可以和第1条同一周期发射。如果一个2-发射2-wide的超标量处理器能在一个周期内同时发射第1条和第3条下一个周期再发射第2条和第4条那同样4条指令标量流水线至少要4个周期超标量可能2个周期就够了。这种隐藏在指令流里的并行性就叫指令级并行Instruction-Level ParallelismILP。ILP的发现方式主要有两种一种靠编译器静态分析在软件层面重排指令顺序另一种靠硬件动态调度在运行时乱序执行、乱序发射。超标量主要走的是后一条路也就是硬件在运行过程中动态寻找并行性。这里有一个很重要的认知程序的ILP是有限度的。不是说发射宽度越大能挖掘的并行就越多。指令之间有真实的数据依赖、分支造成的控制依赖、访存指令之间的地址依赖这些都会限制ILP。现实里一个4发射的乱序处理器跑普通程序能把IPC跑到2到3就已经相当不错了很难达到理论上的4。1.3 从CPI到IPC超标量的关键指标性能公式是CPU时间 指令数 × CPI × 时钟周期。超标量的目标就是降低CPI也就是提高IPC。但要注意这里存在一个彻底的思维转变。在流水线阶段我们关心的核心问题是冒险怎么处理、怎么让流水线尽量不stall目标是CPI尽量接近1。到了超标量阶段目标变成了每个周期平均发射多少条、完成多少条指令也就是IPC比1高多少。这个指标直接决定了处理器的单核性能上限。实际运行中IPC会被四类因素吃掉不少数据冒险、控制冒险、结构冒险比如寄存器堆端口不够、发射槽不够、以及缓存缺失Cache Miss带来的访存停顿。所以看一颗处理器的微架构不要光看发布会PPT上写的“4-wide”“6-wide”发射宽度那个数字是理论最大值。衡量超标量做得好不好最终要看它在真实负载下能把IPC推到多高这个数字才是产品力。2. 超标量流水线的骨架从取指到提交的每一站2.1 取指和译码为什么RISC天生适合超标量x86则要翻译超标量的第一站是取指这里的要求就和标量不一样每个周期必须取回多条指令。假设处理器是4发射取指级每周期至少要拿到4条指令这意味着指令缓存I-Cache的带宽必须足够大不能再像标量那样每次只读一个地址对应的32位或64位数据。通常I-Cache会设计成每周期能读出足够宽的指令块再配合指令缓冲队列把指令连续喂给译码器。译码环节更有意思。RISC指令集天生适合超标量指令定长、格式规整、编码简单译码器可以把指令直接拆成操作数、操作类型、目的寄存器逻辑简单延迟也低。而x86是变长指令一条指令从1字节到15字节都有可能译码极难早期的x86超标量处理器在译码上吃亏不少。x86后来是怎么解决的从Pentium Pro开始x86处理器在译码阶段前面加了一层“预译码/翻译”逻辑把复杂的变长x86指令拆成内部统一的微操作uopmicro-operation。后面整条超标量流水线处理的都是这种定长的uop相当于在头上先做了一层翻译。所以所谓“x86也是RISC核心”这种说法不准确更准确的说法是“x86的取指和译码是一个复杂翻译器翻译完之后的乱序超标量部分本质上是RISC式的”。2.2 译码之后的大排队发射队列、保留站与调度指令经过译码、重命名之后会进入一个非常重要的结构——发射队列也叫保留站Reservation Station。可以把发射队列理解成一个“等待室”每条指令在这里等待它需要的操作数。操作数没准备好就继续等操作数一旦就绪这条指令就变成“可发射”状态等待被调度器选中进入执行单元去执行。调度器每个周期要做几件事扫描发射队列里所有“操作数就绪”的指令按照某种策略选出最多w条w就是发射宽度然后把它们送到对应的执行单元。这个过程叫指令发射调度。这里有个常见的理解误区超标量并不等于乱序执行。很多经典超标量处理器是顺序发射的比如早期的Pentium就是双发射但基本按程序顺序发射。真正让超标量性能大幅提升的是加上乱序执行能力。而现代的高性能处理器几乎都是乱序超标量结合体顺序取指、顺序译码、顺序重命名但发射和执行是乱序的提交则又是顺序的。这也带出一个核心思想乱序执行不是随便乱来而是为了让“已经就绪的指令”不要傻等“还没就绪的指令”先把能干的活干了但最终对外界观察到的结果必须严格按照程序顺序。这个“结果按序”的任务就交给下面要说的提交级和重排序缓冲Reorder BufferROB。2.3 执行和提交乱序执行不能乱序下结论乱序执行带来的最大麻烦是指令执行完成的顺序和程序顺序不一致了。假设程序顺序是A、B、C结果B先算完了A还没算完。如果这时候来个中断CPU怎么知道程序执行到哪一步了如果直接把B的结果写进架构寄存器状态就乱了因为程序是按照A、B、C的顺序期望状态的。为了解决这个问题超标量处理器引入了ROB。它的原理像一个环形缓冲队列译码重命名阶段的每条指令按程序顺序在ROB里占据一个槽位。指令执行完之后把结果标记为“已完成”但先不提交只有当ROB队头的指令变成“已完成”才允许把它真正写到架构状态中也就是提交Commit阶段。这样设计的好处是异常和中断发生时CPU只要把ROB里没有提交in-flight的指令全部作废把状态回滚到最近一条已提交指令那个点就恢复了精确中断状态程序可以从这个点重新开始外部观察不到任何乱序痕迹。ROB的容量很关键它决定了可以同时有多少条指令在流水线里“在途”。现代乱序超标量处理器的ROB通常有128到512项左右。ROB越大能容忍的乱序范围越大越有机会填满发射槽但硬件成本和实现复杂度和做时序收敛的难度都会上升。3. 乱序执行的核心寄存器重命名与三个依赖问题3.1 三条经典冒险只有RAW是“真冲突”流水线那章我们讲过数据冒险这里要再深挖一层。数据冒险其实有三种RAWRead After Write写后读、WARWrite After Read读后写、WAWWrite After Write写后写。RAW是真正的数据依赖后一条指令要读前一条指令写的结果比如前一条算完x1后一条要用x1顺序必须保持这叫真依赖true dependency无法消除只能“等”。WAR和WAW属于名字相关Name Dependency本质上是两个指令用了同一个寄存器名但实际数据流并不需要这个顺序。比如I1: R1 R2 R3 I2: R4 R1 1 # 对I1的R1有RAW必须等 I3: R1 R5 R6 # 和I1写同一个R1WAW I4: R7 R1 R2 # 想读I3要写的R1I1和I3都要写R1如果I3先执行完I1再写R1最终R1的值就是I1的旧值错了——这是WAWI4要读R1但如果I4先执行、I3后执行I4读到的就是旧R1而不是I3的结果这是WAR。如果硬件老实按顺序执行这些指令就白白浪费了很多等待时间ILP就被这些名字冲突吃掉了。3.2 寄存器重命名怎么解决问题寄存器重命名的思路非常直接既然问题是多个指令共用同一个逻辑寄存器名那就多准备一些物理寄存器把逻辑寄存器名动态映射到不同物理寄存器上让“重名”的指令实际上操作的是不同的物理寄存器。比如上面那四行指令如果CPU有16个逻辑寄存器、64个物理寄存器重命名过程大概是这样的架构里有一张寄存器别名表RATRegister Alias Table记录每个逻辑寄存器当前映射到哪个物理寄存器。指令I1要写R1就分配一个空闲的物理寄存器P10更新RAT使R1→P10指令I3也要写R1就再分配一个P20更新RAT使R1→P20。这样一来I1和I3写的是完全不同的物理寄存器WAW冲突消失了。I4读R1时RAT已经指向P20所以I4会等I3的结果和I1没关系WAR冲突也消失了。这就彻底消掉了WAR和WAW剩下的只有RAW真依赖。物理寄存器文件的数量一般比逻辑寄存器多不少常见的设计里物理寄存器数量是逻辑寄存器数量的2到4倍。为什么需要这么多因为重命名不能随便覆盖旧映射旧物理寄存器可能还有没读它的指令必须等所有读它的指令都读完之后才能释放。如果物理寄存器不够用了重命名阶段就必须停顿前面的取指译码也会被堵住这就是一种结构冒险。学习到这里强烈建议自己拿纸笔把上面这个小例子完整推一遍每来一条指令RAT变成什么分配了哪个物理寄存器原来映射的物理寄存器何时释放。推完三条指令寄存器重命名的机制就通了。只读不画很容易觉得懂了实际做题就懵。3.3 Tomasulo算法在超标量里的角色说到乱序执行绕不开Tomasulo算法。它最早用在IBM 360/91的浮点部件上后来成了几乎所有乱序超标量处理器的调度理论基础。Tomasulo的核心是保留站 公共数据总线Common Data BusCDB。保留站里保存指令和它需要的操作数状态。操作数没到就监听CDBCDB上广播的某个结果一旦匹配上自己需要的那个寄存器就把数据捕获到保留站里。所有操作数都到齐了指令就进入“就绪”状态可以被发射到执行单元。现代处理器里CDB这种全局广播的方式已经很少直接使用更多是分布式调度器但“数据流驱动执行”这个思想没变指令能不能执行不取决于前面指令执行到第几步而取决于自己需要的操作数是否已经产生。这个操作数可以来自寄存器堆可以来自执行单元的直接转发旁路网络也可以来自CDB广播或专用的结果总线。我在学习过程中踩过一个坑把Tomasulo和Rename的功能搞混。重命名解决的是寄存器别名和WAR/WAW问题让你摆脱名字冲突Tomasulo解决的是“数据就绪后怎么尽快执行”的调度问题让你摆脱顺序等待。两者是合作搭配的关系重命名保证了后续指令不会因为名字冲突而被迫等待Tomasulo保证了数据一就绪就能发射执行。4. 分支预测和访存系统两个最容易拖后腿的瓶颈4.1 为什么分支预测在超标量里变得生死攸关分支指令会带来控制流的不确定性。标量流水线里每次分支预测错误可能只损失几拍到了4发射、16级流水线的超标量处理器里一次预测错误意味着取指、译码、重命名、发射、执行中所有“在途”指令全部作废可能一次就冲掉几十条指令的执行进度。预测错误率哪怕只差1%在频繁分支的程序里累计下来的性能损失都相当可观。所以现代超标量处理器在分支预测上投入巨大。预测器的构成通常包括分支历史表BHT记录分支跳转的历史常采用两位饱和计数器分支目标缓冲器BTB记录分支跳转的目标地址返回地址栈RAS专门用于call/return指令因为函数返回的地址通常是压栈动态决定的用历史栈可以预测得特别准更高级的两级自适应预测器比如gshare、锦标赛结构则会结合全局分支历史和每一条分支的历史来动态选择预测策略。分支预测准确率在现代高性能处理器里一般能到95%以上甚至接近99%但那一小撮预测错误依然会卡住性能因为它们的惩罚太大了。还有一点容易被忽略乱序执行里预测错误之后不仅要冲刷在途指令还要恢复RAT和ROB状态这个恢复逻辑的快慢也直接影响分支惩罚周期数。4.2 Load/Store队列与访存消歧访存指令的乱序比普通ALU指令更麻烦。Load指令想提前执行但前面Store指令的地址还没有算出来你根本不知道这个Load访存会不会和Store冲突。如果Load先执行了结果读到的是旧数据等Store地址算出来发现地址重叠就晚了。硬件上常见的解决方案是Load-Store队列LSQ。ALU指令可以靠旁路网络快速转发结果访存指令则需要在LSQ里做“访存消歧”Load在执行前先检查之前的Store地址是否可能重叠如果确认不重叠可以提前访存如果不确定就保守等待。当Load和之前的Store地址重叠时Store的结果通常还在存储缓冲区里没写回CacheLoad直接从Store队列里把数据取出来这叫存储转发Store-to-Load Forwarding。访存的一个关键难点是LSQ必须有很强的地址比较能力而且处理逻辑复杂。现代处理器里一个接收宽发射、高主频的访存子系统是最难设计的模块之一。很多程序里Cache缺失造成的停顿比执行单元的空闲还要致命所以超标量的威力往往要配上足够好的Cache层次才能发挥出来。4.3 Cache带宽和其他物理限制多发射对Cache的带宽要求也是成倍上升的。4发射处理器的数据Cache可能需要每周期支持2到4个访问这比单端口Cache复杂得多。多端口Cache的实现方式包括多bank划分、重复读取端口但都会带来面积、功耗和命中率之间的权衡。指令侧也一样I-Cache的带宽必须喂得饱发射宽度否则发射槽必然空转。物理限制不止在Cache上。发射队列越大每周期要做的“选w条指令发射”的仲裁逻辑越复杂时序越难收敛寄存器堆的端口数越多面积和延迟增长越快比如寄存器堆面积的增长大约与端口数的平方成正比。这也是为什么现实主流处理器很少超过6到8发射。8发射以上的乱序超标量处理器功耗和时序代价会急剧上升收益却有限因为程序本身的ILP也很难支撑那么宽的发射。5. 超标量不是唯一路线和VLIW、多核、SIMD的对照5.1 四类并行路线的对比超标量是挖掘指令级并行ILP的一条重要路线但不是唯一路线。拿我学习时整理的对比表来看会更清晰技术路线并行层次并行性由谁发现优点缺点代表超标量指令级ILP硬件动态调度软件透明、二进制兼容硬件复杂、功耗高Intel Core、ARM A7xVLIW/EPIC指令级ILP编译器静态调度硬件简单、功耗低兼容性差、编译器压力大Itanium、部分DSP多核线程级TLP软件提升总吞吐单线程性能提升有限各种多核CPUSIMD数据级DLP编译器/硬件适合数据并行只适合特定负载GPU、NEON、AVXVLIW超长指令字的思路是把发现并行的任务交给编译器。编译器在编译阶段分析出哪些指令可以并行然后打包成一条很长的指令字硬件只需要按照指令字里的并行槽去执行就行。这套路在DSP和某些嵌入式处理器上很有价值因为功耗低、硬件简单。但在通用计算领域Itanium的教训非常深刻二进制兼容困难编译器要精确预测运行时行为和访存延迟几乎不可能实际IPC上不去。多核和SIMD关注的是另外两个层面的并行性。多核跑的是线程级并行适合高并发的服务器负载SIMD关注数据级并行比如图像处理、矩阵运算这类一个操作要套用到大量数据上的场景。它们和超标量不是替代关系而是互补关系。现代高性能处理器都是乱序超标量 SIMD 多核的组合体。5.2 Itanium的教训硬件和软件分工的边界Itanium是EPIC显式并行指令计算路线的代表设计者希望用超长指令字把硬件的复杂度转移到编译器上。理论上很美但在真实负载中访存延迟不确定、分支行为和编译器预期不一致静态调度很难做准。结果Itanium既没有在单线程性能上打过同期的x86超标量处理器生态也没起来最终成了教学案例。这个案例反映出一个深刻的权衡硬件的活可以隐藏复杂度和运行时不确定性但代价是面积、功耗、设计验证成本都高把活推给软件编译器可以简化硬件但前提是软件能拿到足够准确的信息去调度。通用处理器选超标量本质上是用硬件牺牲换来了一件最宝贵的东西——软件兼容性和运行时自适应性。你不需要重新编译程序硬件自己就能把老的、新的、诡异的各类代码的ILP尽量挖掘出来。5.3 为什么主流PC和服务器仍是乱序超标量的天下过去二十年主流PC和服务器处理器几乎都是乱序超标量原因是通用负载的多样性决定了硬件动态调度是性价比最高的方案。程序运行时的行为、Cache命中情况、分支走向都不可能被编译器提前完整预知硬件动态调度却能实时应对。当然在嵌入式、低功耗场景顺序执行或者有限宽度的超标量反而更适合。比如一些低功耗ARM内核会采用顺序双发射配合深度流水线因为乱序执行带来的额外功耗对续航不划算。所以超标量和乱序执行不是必须绑定选择哪种组合完全取决于应用场景要极致单线程性能乱序宽超标量要低功耗低成本顺序窄超标量要极限能效再配合向量化和多核。6. 学习路径、工具和常见误区排查6.1 推荐的入门顺序和资料清单学习超标量有一条比较顺的路径我自己是按这个顺序走下来的先彻底搞懂流水线和数据冒险RAW/WAR/WAW是基础前面不熟后面全是坑。再学超标量最基本的概念发射宽度、IPC、多发射带来的结构冒险。然后学寄存器重命名重点做手推练习。之后学Tomasulo和乱序调度结合保留站、ROB理解执行怎么乱序、提交怎么按序。再去学分支预测、访存队列、指令缓存带宽这些工程细节。最后挑一个具体处理器微架构来对照学习比如RISC-V的BOOM或者翻主流处理器的微架构分析文章。资料方面以下几份是我觉得价值最高的Hennessy Patterson《计算机体系结构量化研究方法》第3章是超标量核心适合建立完整框架。姚永斌《超标量处理器设计》偏硬件设计视角对重命名、ROB、发射队列解释得非常细。各类处理器微架构讲解文章比如讲Intel Golden Cove或者AMD Zen的微架构分析能联系实际。gem5模拟器文档适合动手做实验不过要有心理准备它的配置选项非常多。额外提醒一句在搜索学习资料的时候注意区分“体系结构”这个术语在不同领域的含义。这些年搜“isse-cmm体系结构”或者“软件设计与体系结构”这类词搜出来很多是软件工程、系统工程方向的内容和硬件微架构完全是两回事。别点进去之后发现自己跑偏了。6.2 用模拟器和性能工具观察IPC学超标量不动手验证总觉得隔着一层纱。最方便的实验工具是gem5。用它的O3CPU模型跑一个小程序改几个关键参数就能直观看到超标量的行为。一个示例配置大致是./build/X86/gem5.opt configs/example/se.py \ --cpu-typeO3CPU --caches --l2cache \ --fetch-width4 --dispatch-width4 \ --rob-size192 --cacheline_size64 \ -c ./test_program跑完之后在输出统计里重点看simInsts和simCycles两者一除就得到IPC。你可以把发射宽度从4改成2或者把ROB容量从192改成64再看IPC变化。实测下来你会发现IPC并不是随着发射宽度翻倍而翻倍很多程序在4发射和8发射下的IPC提升非常有限这就是ILP天花板和结构冒险一起作用的结果。如果手头没有gem5用Linux自带的perf也能看自己机器上的实际IPCperf stat -e cycles,instructions ./your_benchmark输出里会有instructions和cycles两者相除就是当前程序在这颗CPU上实际跑出来的IPC。你自己可以去试一下绝大多数通用程序的IPC都在2到3之间很难超过4这比任何教科书都更有说服力。这里有个很重要的实操感悟当你看到实际IPC离标称发射宽度差很远的时候不要觉得是处理器设计有问题。Cache缺失、分支预测错误、数据依赖、发射槽竞争这些都是正常开销。处理器设计的目的不是把IPC推到理论极限而是在功耗、复杂度、成本约束下尽可能逼近ILP收益曲线的拐点。6.3 常见误区速查表最后把我见过最多、我自己也踩过的几个认知误区整理成一张速查表拿走直接用常见误区实际澄清超标量等于多核完全不是。超标量是一个核内部的多发射多核是多个物理核心并行超标量等于乱序执行不对。超标量说的是发射宽度乱序说的是调度策略。顺序超标量也存在比如部分嵌入式RISC核发射宽度越大性能一定越高不一定。ILP有限资源竞争和功耗也会增加超过拐点反而亏寄存器重命名可以消除RAW依赖错。重命名消除的是WAR和WAW这些名字相关RAW是真数据依赖只能等分支预测错误只是浪费几个周期在深流水线宽发射面前一次预测错误可能冲掉几十条在途指令代价极大IPC可以接近发射宽度现实中受各种停顿因素影响实际IPC往往只有发射宽度的三分之一到二分之一这几点每一个都能对应到真实的处理器设计决策上。理解这些误区比记住一堆术语有用得多。说实话我第一次接触超标量的时候被Tomasulo和寄存器重命名绕得晕头转向。后来发现一个管用的笨办法回到具体的指令序列拿笔一步步推把每条指令的物理寄存器分配、保留站占用、ROB状态都画出来几轮下来就通了。另一件事是动手跑模拟器光看书的增益真的很有限只有自己把发射宽度改了、ROB容量调了、看到IPC跟着变化才算真正建立了直觉。这篇就先聊到这里下一章我们会把这些机制落到一个具体处理器的微架构上去。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价