try ai
科普
编辑
分享
反馈
  • Tomasulo算法

Tomasulo算法

SciencePedia玻尔百科
核心要点
  • Tomasulo算法通过使用保留站、寄存器重命名和公共数据总线动态解决数据依赖,从而实现乱序执行。
  • 它通过使用标签重命名寄存器并在指令发射时将操作数值复制到保留站,消除了伪依赖(WAR和WAW冒险)。
  • 该算法在硬件内部将顺序程序转换为动态数据流图,允许指令在其数据可用时立即执行。
  • 该算法的核心原理在软件概念中也有体现,例如编译器的静态单赋值(SSA)和并发编程中的future/promise机制。

引言

在追求更高计算性能的过程中,简单的顺序处理很快成为瓶颈。就像一个管弦乐队,每个音乐家都必须等待前一个人演奏完毕,顺序处理器在独立任务已经准备好运行时,会浪费宝贵的时间。解决方案是乱序执行,这是一种允许处理器同时处理多条指令的范式,但这又引入了管理数据依赖和资源冲突的混乱。本文旨在揭开解决这种混乱的优雅方案的神秘面纱:Tomasulo算法。首先,“原理与机制”一章将剖析其核心组件——保留站、寄存器重命名和公共数据总线——以揭示它们如何在CPU内部创建一个自组织的数据流机器。随后,“应用与跨学科联系”一章将探讨该算法超越硬件的深远影响,将其与编译器理论、推测执行乃至现代软件并发模型联系起来。

原理与机制

想象一个管弦乐队。在传统的交响乐中,指挥家站在前面,规定节拍,确保每个音乐家在精确、预定的时刻演奏自己的部分。一个简单的​​顺序​​执行的计算机处理器就是这样工作的。每条指令都是一个音乐家,而流水线时钟就是指挥家的指挥棒。如果第一小提琴手手忙脚乱地找乐谱,整个乐队就会停滞不前,原地等待。这种方式安全有序,但效率极低,尤其是当长笛部分已经准备好演奏一段优美、独立的旋律时。

如果我们能创建一个没有指挥的管弦乐队呢?一个系统中,每个音乐家只要拿到乐谱、乐器准备就绪,就可以立即演奏自己的部分,而无需等待某个全局信号。这就是​​乱序执行​​的梦想,这一范式有望释放隐藏在一系列指令中的真正并行性。一个能够向前看并在等待指令1的缓慢内存加载完成时,就去执行指令5(一个简单的加法)的处理器,可以获得巨大的性能提升。

但这种自由会带来混乱。我们如何防止一个音乐家在另一个音乐家读完同一乐谱架上的旧乐谱之前就开始演奏新的乐曲?如果有多个作曲家在重写乐曲,我们如何知道哪个版本是“最终”版本?1967年,Robert Tomasulo设计了一种极其优雅的算法来驯服这种混乱,其方法不是重新强加一个严格的指挥,而是给予音乐家们一些巧妙的工具让他们自行协调。他的算法解决了三种基本类型的冲突,即​​冒险​​:

  • ​​写后读(RAW):​​ 真正的数据依赖。音乐家必须等待作曲家写完一段旋律后才能演奏。这是根本性的,必须遵守。

  • ​​读后写(WAR):​​ 一种名相关依赖。一位作曲家想擦掉黑板写一段新旋律,但一位音乐家还在读旧的旋律。冲突在于存储位置的“名称”(黑板),而不是数据本身。

  • ​​写后写(WAW):​​ 另一种名相关依赖。两位作曲家都急于在同一份最终手稿上写下他们的“C大调交响曲”。应该保留哪一个?

Tomasulo算法通过三个相互关联的组件解决了这些问题:保留站、寄存器重命名和公共数据总线。

智能等候室与复制的配方:驯服WAR冒险

在我们的无指挥管弦乐队中,我们不让音乐家排队,而是将他们送到与每种乐器(例如加法单元、乘法单元)相关联的智能“等候室”,称为​​保留站​​。当一条指令被分派时,它会进入一个可用的保留站。

此时,第一个天才之举发生了。指令立即尝试收集其“配料”——即它的源操作数。如果操作数的值在主“储藏室”(架构寄存器文件)中可用,该值将被复制到保留站中。复制这一行为至关重要。这就像厨师给食谱拍了张照片,而不是直接从主食谱书中读取。一旦副本制作完成,厨师就不在乎其他人(后来的指令)是否会重写食谱书的那一页。与物理存储位置的依赖关系被切断了。

这个简单的机制完全消除了所有的读后写(WAR)冒险。在像记分牌这样的旧设计中,如果一条长指令I1正在缓慢地从寄存器F1中读取,而一条快速的、较晚的指令I2想要向F1写入一个新值,I2将被迫等到I1读完为止。有了Tomasulo算法,I1在最开始就将F1的值复制到其保留站中。I2随后可以随时向F1写入,而I1绝无可能获取错误的值。其关键思想在于,通过在指令发射时捕获值,就切断了该源操作数与架构寄存器名称之间的联系[@problem_id:3638586, E]。

重命名的魔力:征服WAW冒险

但是,如果一个操作数还没准备好呢?如果它的值仍在由一条更早的指令计算中呢?在这种情况下,保留站不会得到一个值,而是得到一个承诺——一个占位符。这个占位符是一个唯一的​​标签​​,就像熟食店的取餐号一样,它标识了最终将产生所需值的指令。

这就引出了第二个天才之举:​​寄存器重命名​​。当一条将产生结果的指令(比如 I3: SUB R1, ...)被发射时,它会从其保留站获得一个标签(例如 A2)。一个中央目录,即​​寄存器状态表​​,会立即更新并记录:“R1的官方未来值将来自带有标签A2的指令。”

现在,想象一条更早的指令 I1: ADD R1, ... 也在向R1写入。如果没有重命名,这就是一个写后写(WAW)冒险。哪个R1才是正确的?Tomasulo算法干净利落地解决了这个问题。当I3被发射时,寄存器状态表只是简单地覆盖R1的条目,将其指向I3的标签A2。任何后续需要R1的指令现在都会被告知等待标签A2。

当I1最终完成并用标签A1广播其结果时会发生什么?架构寄存器文件会检查状态表。它看到R1的官方生产者现在是A2,而不是A1。因此,它会直接忽略I1的结果。这个过时的旧值被阻止覆盖架构状态,WAW冒险在没有任何停顿的情况下消失了。名称R1被动态地重新映射到不同的物理存储位置——由标签标识的保留站的结果字段。

“城镇公告员”:通过公共数据总线广播结果

我们现在有了一个由指令组成的系统,它们位于保留站中,有些有值,有些有标签。等待的指令如何获得它们的数据呢?

这是第三个关键组件的工作:​​公共数据总线(CDB)​​。可以把它想象成整个处理器的“城镇公告员”或公共广播系统。当一条指令完成执行时,它不会悄悄地把结果存起来。它会抢占CDB,向所有人大声广播它的结果和它的标签:“注意!注意!标签A1的结果现在可用!值为128!”

每一个保留站都在不断地监听CDB。同时,每个持有标签的操作数字段都会将其标签与正在广播的标签进行比较。这需要大量的硬件——每个等待的操作数槽都需要自己专用的标签比较器。如果一个操作数字段发现匹配,它会立即从CDB上抓取该值,存储它,并将自己标记为“就绪”。

这个广播机制解决了根本的写后读(RAW)依赖。因为CDB是一条广播总线,一个生产者可以同时“唤醒”多个等待的指令。如果指令I3和I4都在等待I1的结果,它们都会监听CDB,当I1广播其结果时,I3和I4都将在同一个周期内获取该值并准备好执行。这种从生产者直接到消费者的数据转发,独立于寄存器文件进行,是数据流执行的心跳。标准模型是,这些数据在CDB广播之后才可用,使得CDB成为转发结果的唯一真实来源。

硅片上的数据流交响曲

当你将这三个部分——保留站、通过标签实现的寄存器重命名和公共数据总线——组合在一起时,你会得到一个令人叹为观止的优雅系统。指令不再受其在程序中的顺序束缚。相反,它们在硬件内部被转换成一个动态的​​数据流图​​。一条指令变成一个独立的代理,它等待其数据依赖关系得到解决,一旦可以就立即触发执行,然后将其结果提供给下一波等待的指令。

处理器动态地发现程序的真正依赖关系,并在物理资源(“炉灶”或功能单元的数量,以及单一的CDB“城镇公告员”)允许的情况下以最快的速度执行它。程序中僵硬的、顺序的乐谱被转变为一场流动的、自组织的数据流交响曲。

最后几块拼图:内存与精度

这个漂亮的机器还不算完整。两个现实世界中的复杂问题依然存在:内存和异常。

我们不能像重命名寄存器那样重命名内存位置。从地址0x1000加载和向地址0x1000存储存在真正的依赖关系。为了处理这个问题,Tomasulo架构增加了一个​​加载-存储队列(LSQ)​​。这个专门的单元跟踪所有待处理的内存操作。它必须足够聪明,以便在存在一个针对未知或相同地址的更早的存储操作时延迟一个加载操作。如果地址匹配,LSQ会安排​​存储到加载的转发​​,数据直接从存储条目发送到加载条目,而无需经过主内存,从而保留了数据流原则。如果地址不同,LSQ则允许加载操作独立进行。

一个更深远的挑战是精确地处理异常。在这个混乱的、乱序的世界里,如果指令I1(一个缓慢的加载)触发了缺页中断,但一条更快的、更晚的指令I2(一个简单的加法)已经完成并将其结果写入了架构寄存器文件,会发生什么?机器的状态现在是“不纯净的”——它反映了一个本不应该发生的未来。这是一种​​不精确异常​​,它使得从错误中恢复几乎不可能。

经典的Tomasulo算法就存在这个缺陷。最终的解决方案,也成为现代CPU的基石,是增加最后一个硬件部件:一个​​重排序缓冲区(ROB)​​。ROB充当已完成结果的暂存区。指令仍然乱序执行并在CDB上广播结果,但这些结果更新的是ROB,而不是最终的架构状态。然后,ROB按原始程序顺序“提交”这些推测性结果到架构寄存器文件和内存。如果一条指令发生故障,ROB只需清空自身以及所有后续的推测性结果,使架构状态保持完全精确,就好像管弦乐队在错误发生的那一刻精确地停了下来一样。

有了这最后的补充,整个旅程就完成了。从一个简单的、僵化的流水线,我们构建了一个动态的、自组织的数据流机器,它快速、高效,并且由于ROB的存在,精确而可靠——这正是驱动当今几乎所有高性能处理器的引擎。

应用与跨学科联系

在窥探了Tomasulo算法的精巧机制后,人们可能倾向于将其归类为一种构建更快处理器的巧妙但小众的技巧。那就错了。这样做就像了解了拱形结构后,只把它看作是建桥的一种方式,而没有欣赏它对大教堂、引水渠乃至整个建筑语言的影响。Tomasulo算法不仅仅是一项硬件设计;它是一种关于计算的深刻思想的体现,这种思想在远超CPU核心范围的领域中回响。这是一段探究依赖与并行本质的旅程。

隐藏延迟的精湛艺术

从本质上讲,现代处理器是一位魔术大师。它最伟大的戏法就是隐藏延迟——物理过程中固有的、不可避免的延迟,尤其是从内存中获取数据那段令人痛苦的漫长旅程。如果处理器每次需要从内存中获取数据时都停下来等待,我们的计算机会感觉慢得令人难以置信。Tomasulo算法是施展这一魔法最有力的工具之一。

想象一个程序,它首先需要从内存加载一个值,然后执行二十个独立的计算,最后才使用这个加载的值。一个简单的顺序处理器就像一个听话但缺乏想象力的文员:它会去取内存值,耐心地等待可能长达200个周期的到达时间,然后才开始那二十个其他计算。

然而,一个配备了Tomasulo算法的处理器则是一位更具事业心的管理者。当它看到加载指令时,它会向内存系统分派请求,并做一个标记——一个“标签”——表示“此操作的结果将被称为‘标签12’”。然后它立即继续前进。看到那二十个独立的计算,它会兴高采烈地执行它们。它们不依赖于‘标签12’,那为什么要等待呢?所有这些工作都是在内存访问进行期间完成的。等到内存值最终到达时,处理器早已完成了其他任务。这200个周期的延迟几乎被完全隐藏了。这就是乱序执行的力量。

当然,这并非隐藏延迟的唯一方法。想想你电脑里的图形处理单元(GPU)。它面临同样的问题,但采用了不同的哲学。GPU不试图在单个任务中寻找独立的工作,而是同时运行成千上万个相似的任务(或“线程”),这些任务被分组成“warp”(线程束)。当一个warp因等待内存而卡住时,GPU调度器不会试图重排它的指令。相反,它会简单而即时地将注意力切换到另一个准备好运行的warp上。它通过同时处理大量的并行任务来隐藏延迟。这是一种线程级并行(TLP)策略,与CPU的指令级并行(ILP)相对。没有哪种方法是普遍“更好”的;它们是针对不同类型问题的不同工具,这也是为什么CPU和GPU演变成了如此独特、专门化的架构。

Tomasulo算法的核心思想被证明具有非凡的灵活性。它不仅适用于简单的单值操作。现代处理器执行复杂的向量操作,将一条指令应用于整个数据数组(一种称为SIMD的技术)。如果输入数据的某些部分已经就绪,而其他部分仍在计算中,会发生什么?整个向量操作必须等待吗?完全不必。Tomasulo算法的逻辑可以被优雅地扩展。硬件可以维护一组标签,而不是整个向量只有一个标签,向量的每个通道都有一个标签。然后,操作可以分块进行,在数据就绪的通道上执行,只等待那些尚未就绪的通道。这需要更复杂的保留站和能够宣告向量哪个特定通道现已就绪的公共数据总线(CDB),但“基于标签广播唤醒”的基本原则保持不变。

这种动态性也是现代CPU中最激进的策略之一——​​推测执行​​——的关键。处理器为了保持忙碌,常常会猜测一个条件分支的结果,并在条件实际确定之前很久就开始执行预测路径上的指令。Tomasulo算法通过为这些推测性指令分配标签来促进这一点。如果猜测正确,结果将无缝集成。如果猜测错误,一个“清除”信号被发送,处理器必须丢弃所有推测性工作。与这些被丢弃的工作相关联的标签必须从所有表和保留站中失效,以确保程序永远不会看到不正确的、推测性的结果。这是一场高风险的赌博,在性能上回报丰厚,但当赌注出错时,需要Tomasulo标签的精细记账来收拾残局。

编译器思想中的回响

真正引人入胜的是,Tomasulo算法背后的核心思想在一个完全不同的领域被独立地发现了:编译器设计。编译器的任务是将人类可读的代码翻译成机器指令。在此过程中,它面临着一个类似的问题。程序员可能会重用一个寄存器名,比如$R1,用于几个不同的、不相关的值。这会产生并非真实数据依赖的“名相关依赖”,并可能不必要地限制指令重排。

为了解决这个问题,现代编译器通常将程序转换成一种称为​​静态单赋值(SSA)​​的中间形式。在SSA形式中,每个变量只被赋值一次。如果一个程序员三次写入$R1,编译器会在内部将它们重命名为$R1_1、$R1_2和$R1_3。听起来很熟悉?这正是Tomasulo算法在运行时所做的事情!硬件为每条产生结果的指令分配一个新标签,有效地为目标寄存器创建了一个新“版本”。SSA和Tomasulo的重命名是同一枚硬币的两面:一种通过为每个计算出的值赋予唯一名称来消除伪依赖的策略,从而揭示程序的真实数据流。

这引出了计算机体系结构中的一个宏大哲学辩论。如果编译器足够智能,能够找出所有依赖关系并静态地调度指令(如在显式并行指令计算(EPIC)架构中),我们就可以构建更简单的硬件,只需执行编译器的完美计划。这将复杂性从硬件转移到了软件。另一种选择是Tomasulo式的方法:使用复杂的、“智能的”硬件在运行时动态地找出调度方案。这种动态方法的优势在于能够对缓存未命中等不可预测的事件做出反应,而静态的编译器调度则无法做到。大多数现代高性能CPU都采用动态方法,押注于复杂硬件的成本是值得其提供的灵活性的[@problem-id:3640788]。

统一原则:数据、流与承诺

然而,最深层次的联系出现在我们退后一步问:Tomasulo算法真正在做什么?它正在将一个程序,一个顺序的指令列表,转变为一个​​数据流图​​。在纯数据流计算模型中,一个操作的执行不是基于它在程序中的位置,而是一旦其输入数据可用就执行。操作是图中的一个节点,数据值是沿边传播的“令牌”。一个节点只有在所有输入边上都收到了令牌后才会“触发”(执行)。

这完美地描述了一个保留站!一个RS条目就是一个数据流节点。操作数字段是它的输入弧。它们等待“令牌”——在CDB上广播的带标签的值。当所有操作数都存在时,指令就“触发”。CDB本身充当令牌分发网络,将结果广播给所有需要它们的节点[@problem--id:3685498]。与纯数据流机器的关键区别在于,Tomasulo算法使用广播-监听机制,所有RS条目都监听CDB以寻找它们感兴趣的标签。这是一个“拉”模型,消费者找到它们的数据,而不是一个将令牌显式路由到特定目的地的模型。

这种与数据流的联系不仅仅是学术上的好奇;它弥合了底层硬件和高层并发编程之间的鸿沟。当你使用​​future​​或​​promise​​编写现代异步代码时,你使用的正是相同的数据流原则。一个“future”是一个尚未计算出的结果的占位符——它相当于一个等待操作数的保留站条目的软件版本。将产生该值的操作就是“promise”。当操作完成时,它“履行”了promise,任何等待该future的程序其他部分都会被通知并可以继续执行。

这是Tomasulo算法的直接软件类比。发射一条指令创建了一个由标签标识的“promise”。CDB广播是该promise的“履行”。等待该标签的其他指令就像等待一个future完成的任务。硬件约束,比如只有一个CDB,类似于软件约束,比如线程池中线程数量有限或保护临界区的锁。构建乱序处理器的挑战与编写高效、正确的并发软件的挑战是相同的,只是在不同的抽象层次上实现而已。

最后,即使是Tomasulo算法的粗糙实现细节也能在别处找到回响。标签是从一个有限的池中提取的。如果你发射指令的速度太快,以至于在等待同一个旧标签的老指令看到其结果之前,就循环使用了所有可用标签并重用了一个,会发生什么?这是一个真正的风险,尤其是在具有物理信号延迟(skew)的大型系统中。为了防止这种“标签混叠”,系统必须设计成确保标签空间足够大,以至于在最大可能的延迟窗口内不会被耗尽。这可能涉及节流指令发射或向标签添加“纪元”位以区分标签池的不同循环周期。这与分布式系统中尝试在没有中央权威的情况下生成唯一事务ID时面临的问题从根本上是相同的;在这两种情况下,你都必须在一个动态、异步的环境中管理一个有限的命名空间。

从隐藏内存延迟的技巧,到组织向量硬件的原则,再到编译器理论的平行体现,最终到数据流和并发模型的物理实现——Tomasulo算法证明了计算机科学中伟大思想的统一之美。它提醒我们,在一个抽象层次上思考问题的正确方式,往往在所有地方都是思考问题的正确方式。