try ai
科普
编辑
分享
反馈
  • 结构冒险

结构冒险

SciencePedia玻尔百科
核心要点
  • 当流水线中的两条或多条指令在同一时钟周期内需要相同的物理硬件资源时,就会发生结构冒险,从而导致停顿。
  • 一个常见的例子是对统一内存系统的冲突,这通常通过实现具有独立指令和数据缓存的哈佛架构来解决。
  • 与因数据流中的依赖关系而产生的数据冒险不同,结构冒险纯粹是关于物理硬件组件的可用性问题。
  • 解决结构冒险的方法包括复制资源、对有争议的单元本身进行流水线化,或使用巧妙的时序方案,如时钟周期拆分访问。
  • 资源争用的原则是普遍存在的,它不仅限于CPU,还延伸到多核系统、软件构建过程和其他复杂系统中的瓶颈问题。

引言

在对计算速度不懈追求的过程中,计算机架构师设计出了流水线技术——这项卓越的技术允许处理器同时处理多条指令,就像一条装配线。这种并行性极大地提高了指令吞吐率和整体性能。然而,这种高效率也带来了一系列新的挑战,即所谓的“冒险”,它们会扰乱流水线的顺畅运行。虽然一些冒险与数据流或控制逻辑有关,但本文专注于一个更具体的问题:硬件本身的物理限制。

本文探讨了结构冒险这一基本问题,它源于不同指令在同一时间争用同一硬件部件。我们将探讨这些处理器内部的“交通堵塞”如何造成降低性能的停顿。读者将深入理解什么是结构冒险、如何识别它们,以及架构师为减轻其影响而采用的巧妙设计方案。以下章节将首先剖析这些冒险的核心原理和机制,然后拓宽视野,审视其深远的应用和跨学科关联性。

原理与机制

现在我们对主题有了大致的了解,让我们卷起袖子,深入探究其内部工作原理。计算机体系结构的世界,如同物理学一样,由几个极其简单却又异常强大的原则所支配。其中之一就是有限资源的概念。你不能同时身处两地,两个物体也不能同时占据同一个空间。在处理器中,这个简单的真理引出了我们所说的​​结构冒险​​。

宇宙级的交通堵塞:什么是结构冒险?

想象一个高科技汽车工厂,拥有一条效率极高的装配线。每辆汽车都经过一系列工位:底盘组装、发动机安装、喷漆、内饰安装和最终检验。这就是一条流水线。其神奇之处在于,当一辆车在喷漆时,另一辆车正在安装发动机,第三辆车则在搭建底盘。许多汽车被同时加工,每小时都有一辆成品车下线。生产一辆车的时间可能是五小时,但吞吐率是每小时一辆车。

现在,假设发动机安装工位和内饰安装工位都需要同一把高度专业化的机器人扳手。在某个小时内,发动机工位的汽车需要这把扳手,而内饰工位的汽车也需要它。会发生什么?我们遇到了交通堵塞。一个工位必须等待。装配线戛然而止,产生了一个空闲的气泡,在那个小时里,没有新车从生产线末端出来。

这简而言之就是​​结构冒险​​。它是一种冲突,仅仅因为我们流水线的多个部分在完全相同的时间需要同一个物理硬件——同一把“机器人扳手”。其后果是​​停顿​​(stall),即流水线流程中的瞬间暂停,这会损害性能。我们用一个名为​​每指令周期数(Cycles Per Instruction, CPI)​​的指标来衡量这种性能影响。在完美的流水线中,CPI为1,意味着每个时钟周期完成一条指令。停顿会增加这个数字,告诉我们平均来看,完成一条指令需要超过一个周期。

图书馆大辩论:指令 vs. 数据

让我们从工厂转向处理器。最常见且争用最激烈的资源之一是计算机的内存系统。可以把它想象成一个巨大的图书馆。在我们经典的五级流水线(取指、译码、执行、访存、写回)中,有两个不同的阶段总是在敲图书馆的门。

  1. ​​取指(Instruction Fetch, IF)​​阶段:它的工作是去图书馆获取下一条指令(CPU要遵循的下一个“食谱”)。
  2. ​​访存(Memory Access, MEM)​​阶段:在流水线中晚四个步骤的这个阶段,可能需要为一条已经深入处理的指令访问图书馆。例如,一条load指令需要从图书馆获取数据,而一条store指令需要向图书馆写入数据。

冲突就在于此。在同一个时钟周期内,IF阶段试图取指令I4I_4I4​,而MEM阶段则试图为指令I1I_1I1​访问数据。如果图书馆只有一个门——一个​​单端口统一内存​​——我们就遇到了结构冒险。

CPU必须做出选择。通常,它会优先处理流水线中更靠后的指令,即处于MEM阶段的指令。IF阶段被迫停顿一个周期。这会插入一个​​流水线气泡​​,一个在系统中传播的空槽。如果像一项分析显示的那样,一个程序中44%的指令是加载或存储指令,那么流水线将不得不在其近一半的生命周期中停顿!理想的CPI=1会膨胀到1.44,这代表了44%的性能下降,而这一切都仅仅因为这一个交通堵塞。

解决图书馆问题:更多的门和更好的缓存

那么,架构师如何解决这个问题呢?最优雅的解决方案是认识到我们要求图书馆做两件不同的事:提供指令和提供数据。为什么不给它两扇门呢?这一绝妙的见解催生了​​哈佛架构​​,它为指令和数据设置了独立的内存通路。IF阶段使用自己私有的门(指令缓存),MEM阶段使用它的门(数据缓存)。它们永远不会冲突。通过拆分资源,结构冒险就完全消失了。对两种设计的比较揭示了这一选择的严酷现实:在具有35%内存操作的典型工作负载下,统一缓存的机器可能比其分离缓存的同类产品慢35%。

但如果在图书馆上建一个全新的侧厅太昂贵了怎么办?一个巧妙且更便宜的替代方案是专门为指令建造一个小型、快速的“阅览室”,称为​​指令缓存​​。大多数时候,IF阶段需要的指令已经在这个近旁的缓存中了。只有在罕见的“缓存未命中”时,它才需要去主图书馆的门,而那里它可能需要等待。这并不能消除冒险,但极大地降低了其发生的频率。即使一个指令缓存只能满足我们50%的需求(h=0.5h=0.5h=0.5),也能将这种冒险导致的停顿次数减半,使我们的CPI从1+pLD/ST1+p_{\text{LD/ST}}1+pLD/ST​改善为1+12pLD/ST1 + \frac{1}{2} p_{\text{LD/ST}}1+21​pLD/ST​,其中pLD/STp_{\text{LD/ST}}pLD/ST​是内存指令的比例。

抄写员的困境:兼顾读写

另一个关键的共享资源是​​寄存器文件​​——一个小型、超快速的草稿板,CPU在这里保存当前的工作数据。就像内存一样,有两个阶段会来敲门。

  1. ​​指令译码(Instruction Decode, ID)​​阶段需要读取源寄存器的值,为操作做准备。
  2. ​​写回(Write Back, WB)​​阶段需要将已完成操作的结果写回到目标寄存器中。

在给定的周期内,WB阶段的指令可能正在写入寄存器R1,而ID阶段的后续指令正试图从R2和R3读取。这是另一个潜在的结构冒险。我们需要两个独立的寄存器文件吗?幸运的是,不需要。解决方案是微体系结构工程的杰作。

首先,我们构建一个​​多端口​​寄存器文件,给它多个“笔”来写入——比如,两个专用的读端口和一个专用的写端口。这提供了物理能力。其次,我们采用​​时钟周期拆分​​操作。我们将我们微小的时钟周期(可能只有纳秒的一小部分)分成两半。WB阶段的写操作被指定在周期的前半部分进行。ID阶段的读操作在后半部分进行。通过在同一周期内调度它们的访问,我们让两者都能在没有冲突的情况下进行。这就像一场精心编排的舞蹈,一个通过巧妙的时序而非暴力复制来解决资源冲突的优美范例。

区分敌友:辨别冒险类型

结构冒险的定义很简单——硬件资源冲突。但在实际中,它们有时会像伪装大师一样,与它们的近亲——​​数据冒险​​看起来惊人地相似。数据冒险是关于指令之间的逻辑依赖关系,即数据本身的流转。区分它们是关键。

考虑一个先进的乱序处理器。想象三条指令同时发射:

  • I1I_1I1​: MUL R1 - R2 * R3 (一个慢速乘法)
  • I2I_2I2​: ADD R4 - R5 + R6 (一个快速加法)
  • I3I_3I3​: ADD R1 - R7 + R8 (另一个快速加法)

一个周期后,两个快速加法I2I_2I2​和I3I_3I3​都已完成,并准备好写回它们的结果。但处理器只有一个写端口到寄存器文件。I2I_2I2​想写入R4,I3I_3I3​想写入R1。它们同时需要单个写端口,这是一个纯粹的​​结构冒险​​。

但再仔细看。I1I_1I1​和I3I_3I3​之间还有另一个更微妙的问题。两者都想写入同一个寄存器R1。I1I_1I1​在程序中排在前面,所以它的结果应该是R1最后持有的值。然而,由于I3I_3I3​快得多,它先完成,并在I1I_1I1​完成之前很久就准备好写入R1。如果我们让I3I_3I3​写入,然后又让I1I_1I1​写入,我们就会用一个陈旧的结果覆盖正确的结果。这种潜在的错误数据流是一个​​写后写(WAW)数据冒险​​。第一个冲突是关于硬件可用性;而这个是关于维护程序的逻辑正确性。

这种混淆也可能发生在其他共享单元上。想象一个只有一个​​地址生成单元(AGU)​​的处理器,这是计算内存地址的专用计算器。现在考虑这两条背靠背的指令:

  • I1I_1I1​: load R4 - Mem[R1 + 0]
  • I2I_2I2​: store Mem[R1 + 8] - R5

两条指令都使用寄存器R1。这是关于R1的数据冒险吗?不是!两条指令都没有改变R1。它们都只是读取它。真正的冲突,即结构冒险,是两条指令在同一时间都需要唯一的一个AGU来执行它们的+ 0和+ 8计算。冲突在于计算器,而不是R1中的数据。首要原则总是:争夺的是否是物理硬件?如果是,那就是结构冒险。

当冒险相撞:完美风暴

在简单的分析中,我们逐一审视冒险。实际上,现代处理器是一个混乱的系统,所有事情都可能同时发生,不同类型的冒险可能相互碰撞,造成性能损失的“完美风暴”。

一些资源是如此紧张的瓶颈,以至于它们决定了整个机器的节奏。假设我们发明了一种新的“三元”指令,需要同时从三个源寄存器读取。如果我们精密的寄存器文件只有两个读端口,会发生什么?ID阶段将被迫花费整整两个周期来收集其操作数。无论流水线的其余部分有多快,这台机器都不可能在每两个周期内完成超过一条这样的指令。CPI永远不可能优于2。双端口读出成为性能的根本限制器。

现在来看真正混乱的场景。想象一个控制冒险——CPU错误预测了分支将走向何方,必须清空流水线并从正确的路径开始取指。这本身代价已经很高。但如果获取那条正确指令导致了I-cache(指令缓存)未命中呢?现在我们需要去下一级内存(L2缓存)获取它。但如果通往L2缓存的单一共享端口已经在为一条更早指令的长时间D-cache(数据缓存)未命中提供服务呢?现在我们从控制冒险中恢复的过程被一个结构冒险所停顿。CPU卡住了,等待一个本身也在等待其他东西的资源。

这是一个​​复合冒险​​。总的惩罚不仅仅是各部分之和;它更糟。架构师使用概率模型来理解这些相互作用。对这样一个场景的分析表明,这种特定的结构冲突——一个I-cache填充被一个D-cache填充阻塞——平均每条指令会额外增加0.1728个周期的延迟,这纯粹是由于两种冒险相互干扰造成的。这是一个严峻的提醒:在追求性能的道路上,我们不仅是在与停顿和延迟进行单独的战斗,更是在与它们复杂且往往出人意料的相互作用进行一场战役。

应用与跨学科联系

在我们迄今为止的旅程中,我们已经探讨了结构冒险的内部工作原理——当多条指令同时需要同一硬件时,在处理器内部发生的交通堵塞。我们看到,它们是使用有限资源构建复杂机械的必然结果。现在,我们将拓宽视野。我们将看到,资源争用这一原则不仅仅是芯片设计师面临的一个技术难题,而是一个普遍的主题,它回响在计算的不同层次,甚至延伸到与硅无关的系统中。通过考察它的应用,我们将发现这个单一思想以优雅且时而令人惊讶的方式展现自己,揭示了高效系统设计中一种美妙的统一性。

机器的心脏:资源的交响乐

想象一个单处理器核心是一个小而极其快速的交响乐团。每个音乐家都是一个功能单元——一个加法器、一个乘法器、一个内存加载器。乐谱是程序的指令流。为了让乐团以惊人的速度演奏一首曲子,指挥家(处理器的控制逻辑)必须尽可能高效地将任务分配给音乐家。但如果乐谱要求在一个只有一个的乐器上进行一段漫长而复杂的独奏,会发生什么?

这正是一个非流水线硬件除法器所面临的情景。考虑一个可以执行除法操作的简单处理器,但其除法器单元是一个单一的、不可分割的模块,需要占用(比如说)202020个时钟周期来完成其任务。如果程序包含一连串的除法指令,就会出现一个重大的结构冒险。第一条除法指令占据了除法器单元。第二条、第三条以及所有后续的除法指令都必须排队等待,无所事事。流水线的前端完全停顿,因为前进的道路被阻塞了。即使其他音乐家,比如加法器,都处于空闲状态,整个演奏也因等待那个独奏家而停滞不前。如果我们执行一个仅包含八个这样除法的序列,总时间将被这种等待所主导,膨胀到超过160个周期。

正如我们已经暗示的,解决方案是对除法器本身进行流水线化——将其20个周期的任务分解为20个单周期阶段。现在,即使之前的除法指令仍在进行中,新的除法指令也可以在每个周期进入该单元。单次除法的总时间(其延迟)保持不变,但吞吐率却飞速提升。我们那八个除法的序列现在仅需31个周期即可完成,性能提升超过5倍!这阐明了一个深刻的原则:为了最大化吞吐率,启动间隔(initiation interval)——即一个单元接受新工作的速率——通常比任何单个工作的延迟更重要。一个系统的性能不是由其最慢的单个操作决定的,而是由其最受限制的瓶颈的服务速率决定的。如果一个功能单元每IIIIII个周期只能开始一个操作,那么无论每个操作一旦开始后有多快,或者在它前面设置了多大的等待队列,其最大吞吐率都从根本上被限制为每周期1II\frac{1}{II}II1​个操作。

在现代超标量处理器中,这个交响乐团规模庞大,音乐也极其复杂。结构冒险以更多微妙的形式出现:

  • ​​入口(译码):​​在指令被执行之前,它们必须从程序的机器语言解码为内部的微操作。译码器具有有限的带宽;它每个周期只能处理这么多指令或微操作。如果来了一串异常复杂的指令,每条都扩展成许多微操作,它们就可能超出译码器的能力。这是一个概率性的结构冒险——不是必然发生,而是设计师必须建模和缓解的统计风险,以防止处理器的“前门”成为瓶颈。
  • ​​舞台(发射逻辑):​​在乱序处理器中,可能有一池的指令准备好执行,但每个周期将它们分派到功能单元的“发射槽”数量有限。如果有五条指令准备就绪,但发射宽度只有三,那么即使它们所需的功能单元是空闲的,也有两条必须等待。发射逻辑本身就是一个关键的共享资源。
  • ​​内存的前门(缓存体):​​数据缓存不是一个单一的整体块;它通常被划分为多个缓存体(bank)以允许并行访问。然而,如果两条加载指令恰好在同一周期需要来自同一个缓存体的数据,就会发生结构冒险。只有一个能得到服务,另一个必须等待。这要求处理器的记分板或调度器足够聪明,不仅要跟踪哪些寄存器在使用,还要逐周期地跟踪哪些内存体正忙,或许可以使用一个资源可用性向量来防止这些冲突。
  • ​​数据高速公路(公共数据总线):​​一条指令完成后,其结果必须传递给所有等待它的其他指令。这通过一个称为公共数据总线(Common Data Bus, CDB)的共享通信网络进行。如果几条指令在同一周期完成,它们都会争相广播其结果。如果CDB的带宽有限——比如说,它每个周期只能承载两个结果——就会发生“交通堵塞”。一个结果可能已经就绪,但它必须等待一个周期才能上总线,从而延迟了所有急切等待其值的相关指令。

并行性的编排

到目前为止,我们已经看到硬件如何动态地处理资源。但还有另一种哲学:静态调度,其最佳范例是超长指令字(Very Long Instruction Word, VLIW)架构。在这里,编舞者不是硬件,而是编译器。编译器将独立的操作组合成大的“指令包”,指令包中的每个操作都预定在同一周期在不同的功能单元上执行。硬件更简单;它只是按给定的方式执行指令包。避免结构冒险的重担完全落在了编译器身上。它必须分析每一个操作的资源需求——需要多少ALU、多少内存端口、多少寄存器文件端口——并将它们打包成永远不会超过机器单周期容量的指令包。一个包含两个内存操作的指令包,对于一台只有一个内存端口的机器来说是无效的,将被拒绝。编译器必须将其分解成两个独立的指令包,安排在两个周期内执行。这代表了一个根本性的权衡:硬件复杂性与编译器复杂性。

当我们从单线程执行转向多线程和多核时,资源共享的挑战变得更加突出。在细粒度多线程处理器(或称“桶形处理器”)中,来自不同线程的指令逐周期交错执行。如果两个线程恰好都发出内存访问请求,并在同一时间到达单一共享的加载/存储单元(LSU),我们就遇到了结构冒险。现在,硬件需要一个仲裁器来决定谁先走。一个简单的、总是偏袒线程0的固定优先级仲裁器是灾难的根源。线程1会发现其内存访问请求被永久拒绝,因为线程0总有另一个请求紧随其后。这会导致饿死(starvation)。一个公平的策略,如轮询(round-robin),是必不可少的,它能确保从长远来看,每个线程都能获得对竞争资源的平等份额。

将此放大到现代的片上多处理器,比如有8个核心。所有核心最终共享同一个片外内存系统,通过一个共享的三级缓存和一个单一的内存控制器来访问。这些共享资源是结构冒险的主要来源。L3缓存中用于跟踪对主内存的未决未命中的未命中状态保持寄存器(MSHR)池是有限的。内存控制器本身也只能以一定的速率服务请求。如果所有核心共同产生的内存未命中速度超过了内存控制器能处理的速度,系统就会被压垮。利用排队论的原理,如利特尔法则(Little's Law),架构师可以计算出系统能够处理的最大可持续请求率。为了使系统保持稳定,总的未命中到达率λtot\lambda_{\text{tot}}λtot​必须小于或等于瓶颈资源的最大服务率μmax\mu_{\text{max}}μmax​。例如,如果内存控制器每周期能服务μMC\mu_{\text{MC}}μMC​个请求,我们必须确保λtot≤μMC\lambda_{\text{tot}} \le \mu_{\text{MC}}λtot​≤μMC​。这可能转化为一种“基于信用的流控制”方案,其中每个核心被赋予一个允许的未决未命中预算,以防止任何单个核心垄断共享内存系统并确保全局稳定。

超越硅基:一个普遍原则

此时,您可能会认为结构冒险只是微处理器架构师关心的一个小众问题。但这个概念真正美妙之处在于其普遍性。流水线、依赖和资源争用的原则适用于任何将任务分解并分阶段处理的系统。

考虑一个软件构建系统——编译你的代码的过程。让我们想象一个带有编译阶段和链接阶段的流水线系统。我们有两个“编译器工人”(就像两个ALU)和一个“链接器工人”。

  • 如果一个代码模块M3M_3M3​需要一个头文件H1H_1H1​,而H1H_1H1​是由编译另一个模块M1M_1M1​生成的,我们就有一个真依赖。M3M_3M3​在M1M_1M1​完成编译之前无法开始编译。这完美地类比了写后读(RAW)数据冒险。
  • 如果由于一个错误,所有编译器工人都被配置为将其输出的目标文件写入同一个临时路径,我们就有了一个问题。如果我们并行编译M1M_1M1​和M2M_2M2​,它们会竞相写入该文件,最后一个完成的会覆盖另一个的工作。这是对同一命名资源的多次写入之间的冲突,完美地类比了写后写(WAW)冒险。解决方案与处理器中的相同:重命名。我们为每次编译提供一个唯一的输出文件,解决这个“伪”依赖。
  • 最后,我们只有两个编译器工人和一个链接器工人的事实是一种资源限制。并行编译的数量被限制为两个,而链接一次只能进行一个。这是一个结构冒险。

为了找到构建项目的最快方法,我们必须在尊重所有这些冒险的情况下调度任务。我们可以并行编译M1M_1M1​和M2M_2M2​。一旦M1M_1M1​完成,我们就可以开始编译M3M_3M3​。只有当所有三个都完成后,唯一的链接器才能开始其工作。我们用来解决这个软件后勤问题的逻辑,与处理器调度器用来执行指令的逻辑是相同的。

这就是一个基本思想的真正力量。公共数据总线上的交通堵塞、对软件链接器的争用、超市收银台的排队,或是汽车汇入高速公路——所有这些都是管理共享资源争用这一根本问题的不同面貌。通过理解处理器中的结构冒险,您已经获得了一个透镜,用以观察和理解您周围无数系统中的性能瓶颈。这就是科学内在的美丽与统一。