try ai
科普
编辑
分享
反馈
  • 多核架构:核心社会详解

多核架构:核心社会详解

SciencePedia玻尔百科
核心要点
  • 转向多核架构是为了应对“功率墙”——一个阻止了单核时钟频率继续提升的功耗物理极限。
  • 缓存一致性协议(如 MESI)对于确保共享内存多核系统中多个私有缓存之间的数据一致性至关重要。
  • 程序员必须应对伪共享和内存重排等微妙的性能问题,这需要仔细的数据结构设计和同步机制。
  • 高效的并行性能既依赖于划分工作的算法设计,也依赖于能够感知缓存亲和性等硬件特性的智能操作系统调度。

引言

多年以来,计算能力的进步似乎毫不费力,每一代新处理器都能提供更快的性能,而无需对软件进行任何更改。这个由 Dennard 缩放定律推动的“免费午餐”时代,在 2000 年代中期戛然而止,因为架构师们撞上了“功率墙”——一个无法逾越的热量障碍,阻止了单个处理器的速度变得更快。这场危机迫使处理器设计发生范式转变,从单个强大的核心转向在单个芯片上集成多个更高效的核心。本文深入探讨多核架构的世界,探索定义现代计算的基本原理和实际应用。

第一章“原理与机制”将深入剖析多核设计背后的物理学和逻辑,探讨为何分散工作在功耗效率上更优。我们将研究缓存一致性这一关键挑战以及解决该问题的优雅的 MESI 协议,以及伪共享和内存重排等困扰并行程序的微妙性能陷阱。随后,“应用与跨学科联系”一章将探讨软件和算法是如何设计来利用这种并行性的。我们将看到任务如何被划分,同步如何被管理,以及操作系统在协调性能方面扮演的关键角色,最后展望异构计算的未来。

原理与机制

几十年来,计算的故事很简单:每一代新处理器都奇迹般地变得更快。程序员可以编写他们的代码,等上一两年,然后发现代码在新的硬件上运行速度快了一倍,而无需更改一行代码。这个通常被称为“免费午餐”的神奇时代,是由一个名为​​Dennard 缩放定律​​的原则所驱动的。本质上,随着晶体管的缩小,它们的功耗也按比例缩小,这使得我们可以在不让芯片熔化的情况下提高时钟速度,并将更多晶体管封装在同一空间内。但在 2000 年代中期左右,免费午餐结束了。这个魔法不再灵验。

功率墙与新纪元的黎明

随着晶体管变得难以想象地小,即使在它们应该关闭时也开始漏电。Dennard 缩放定律失效了。得益于摩尔定律的 relentless march,我们仍然可以在芯片上封装更多的晶体管,但我们再也无法让它们全部运行得更快。试图提高单个单片处理器核心的时钟速度会导致热量的灾难性增加。这就是“功率墙”,一个无法逾越的热量障碍。

因此,计算机架构师面临一个深刻的问题:如果摩尔定律给了我们数十亿个晶体管,但我们不能用它们来使单个核心更快,那么我们应该用它们来做什么?答案是革命性的:我们不再建造一个极快、功耗巨大的大脑,而是建造一个“核心社会”——在单个芯片上集成多个更简单、更慢但功耗效率更高的处理核心。

这不仅仅是一个直观的想法;它是计算基本物理学的结果。现代处理器核心的动态功耗大致与其电压的平方乘以其频率成正比(P∝V2fP \propto V^2 fP∝V2f),而其频率大致与其电压成正比(f∝Vf \propto Vf∝V)。将这两者结合起来,功耗大致与频率的立方成正比。这意味着速度的小幅增加需要功耗的大幅提升。

想象一个总功耗预算为 808080 瓦的芯片。假设我们有一个高性能单核心。在它的最高速度下,它可能会消耗 151515 瓦并完成一定量的工作。现在,如果我们改用两个核心呢?由于总功耗必须共享,我们必须以较低的电压和频率运行它们。但由于非线性缩放效应,每个核心速度的下降远没有功耗下降得那么严重。事实证明,两个核心,每个以原始速度的 90% 运行,可能每个只消耗 777 瓦。它们共同消耗 141414 瓦,但提供了 2×0.9=1.82 \times 0.9 = 1.82×0.9=1.8 倍的总计算吞吐量。这就是并行的魔力。通过将工作分散到更多核心上,即使每个核心单独看更慢,我们也能在固定的功耗上限下实现更高的整体性能。这种权衡催生了​​暗硅​​(dark silicon)的概念:在任何给定时间,芯片上必须保持断电状态以维持在散热预算内的那部分晶体管。多核架构是我们为尽可能点亮这些硅片而设计的巧妙方法。

独立思想的社会

多核处理器究竟是什么?理解它是一个​​多指令多数据流(MIMD)​​机器至关重要。这意味着每个核心都有自己独立的“大脑”——自己的程序计数器(PC)——并且可以对自己的数据集执行完全不同的指令流。可以将其想象成一个工人团队,每人都有自己的待办事项清单。这与 GPU 中常见的​​单指令多数据流(SIMD)​​架构有着根本的不同,后者更像一个教官大喊一声命令,然后由一大队简单的士兵同步执行。

MIMD 核心的独立性是它们最大的优势,使它们能够处理复杂和不规则的任务。但正是这种独立性创造了多核时代最大的挑战:通信与协调。而协调的媒介就是共享内存。

巨大挑战:缓存一致性

为了避免每次操作都痛苦地缓慢地访问主内存,每个核心都配备了自己小巧、私有且速度极快的内存,称为​​缓存​​。当一个核心需要数据时,它首先检查自己的缓存。如果数据在那里(一次“命中”),一切都好。如果不在(一次“未命中”),它会从一个更大、更慢的共享缓存或主内存中获取数据,并存储一份副本以备将来使用。

症结就在这里。想象一下,核心 A 读取内存地址 0x1000,其中包含值 5,并将其复制到自己的私有缓存中。片刻之后,核心 B 也读取同一地址,并也获得了一份 5 的副本。现在,如果核心 A 决定将该值更新为 10 会发生什么?它将 10 写入自己的私有缓存。但核心 B 的缓存中仍然保存着过时的值 5。如果核心 B 使用这个值,程序就会出错, chaos 就会随之而来。

这就是​​缓存一致性问题​​。为了构建一个功能性的多核系统,架构师必须保证这种情况永远不会导致不正确的行为。系统必须强制执行一个基本的不变式:对于任何数据片段,只能有​​一个写入者​​或​​多个读取者​​,但绝不能同时存在两者。

解决方案是一套规则,一套缓存之间通信的协议,称为​​缓存一致性协议​​。最常见的协议族是 ​​MESI​​,以缓存行可以处于的四种状态命名:

  • ​​Modified (M):​​(已修改)此缓存拥有该数据的唯一副本,并且它已被更改。主内存的数据已过时。
  • ​​Exclusive (E):​​(独占)此缓存擁有该数据的唯一副本,但它未被更改。它与主内存一致。
  • ​​Shared (S):​​(共享)此缓存拥有该数据的一份副本,并且至少有另一个缓存也拥有副本。所有副本都是干净的(与内存一致)。
  • ​​Invalid (I):​​(无效)此缓存行不包含有效数据。

当核心 A 想要写入一个处于共享状态的行时,它不能直接这么做。它必须首先声明其成为“唯一写入者”的意图。它通过片上互连广播一个“Read-For-Ownership” (RFO) 请求或无效化请求。当核心 B 收到此消息时,它必须将其副本标记为无效(S→IS \to IS→I)。只有在收到所有共享者的确认后,核心 A 才能执行其写入操作并将其行的状态升级为已修改(S→MS \to MS→M)。这个优雅、自动化的对话确保了系统对内存的视图保持一致。

机器中的幽灵:微妙的性能陷阱

虽然一致性协议确保了正确性,但它们可能引入微妙而令人抓狂的性能问题。这些就是可能困扰并行程序、使其 mysteriously 变慢的“机器中的幽灵”。

伪共享

这些幽灵中最臭名昭著的是​​伪共享​​。一致性协议不是对单个字节进行操作;它们操作的是被称为​​缓存行​​的数据块,通常是 64 字节长。这通常是件好事,因为它将一次内存获取的成本分摊到更多数据上。但它也有阴暗面。

想象一下,核心 A 正在循环中反复递增一个计数器 x,而核心 B 在同一芯片上独立地递增一个计数器 y。从逻辑上看,这些操作是完全独立的。但如果 x 和 y 碰巧在内存中相邻存储,以至于它们落在了同一个 64 字节的缓存行上呢?

每当核心 A 写入 x 时,它的缓存必须获得该行的独占所有权。为此,它发送一个无效化消息。持有同一行以访问 y 的核心 B 的缓存接收到无效化消息并将其标记为无效。片刻之后,当核心 B 想要写入 y 时,它发现它的副本是无效的。它现在必须获取该行,这反过来又会使核心 A 的副本无效。缓存行在两个核心之间疯狂地“乒乓”往返,一个核心的每次写入都会导致另一个核心的缓存未命中。这会产生大量隐藏的一致性流量,严重降低性能,即使程序的逻辑完全合理。

这个问题可能更加隐蔽。一个定期更新一个变量的写入线程,可能导致几十个只读取同一缓存行上相邻、不相关数据的读取线程,同时遭遇一致性未命中。解决方案通常是软件层面的:程序员必须意识到缓存行的大小,并在数据结构中添加填充,以确保不同线程使用的独立数据驻留在不同的缓存行上。

内存重排

一个更幽灵般的现象源于​​内存一致性模型​​。为了隐藏写的延迟,现代核心不会等待写操作到达主内存。它们将值写入本地的​​存储缓冲区​​(store buffer)然后继续执行。这是一个强大的优化,但它意味着在一个核心上看起来是按某个顺序执行的操作,可能并非以该顺序对其他核心可见。

考虑一个经典的生产者-消费者场景。核心 A 生成一些数据,然后设置一个标志表示数据已就绪:

loading

核心 B 等待该标志,然后消费数据:

loading

你可能会期望 result 总是 42。但在一个采用​​宽松一致性模型​​的机器上,这并不能保证!核心 A 可能将其 data = 42 的操作放入其存储缓冲区,而 flag = 1 的写入操作绕过它并首先对核心 B 可见。核心 B 随后可能退出循环,在 data 的新值从核心 A 的存储缓冲区提交之前读取 data,从而看到一个过时的值。

为了防止这种情况,程序员必须使用​​内存屏障​​(memory fences 或 barriers)。屏障是一种强制排序的指令。通过在核心 A 的两次写入之间放置一个释放屏障(release fence),程序员告诉硬件:“确保此屏障之前的所有内存操作在它之后的任何操作之前都全局可见。”同样,在核心 B 的循环之后放置一个获取屏障(acquire fence)告诉硬件:“在此屏障之后不要执行任何内存读取,直到与相应 release 发生之前的操作可见为止。” 这种 release-acquire 配对重新建立了程序员意图的逻辑顺序,确保在 buffered writes 和乱序执行的世界中的正确性。

距离的物理现实

随着芯片发展到包含数十个核心,另一个简单的事实变得显而易见:距离很重要。认为存在一个单一、共享且延迟相同的“L3 缓存”是一种幻觉。实际上,这些大型缓存被切片并物理分布在芯片上,这种设计被称为​​非均匀缓存架构(NUCA)​​。访问位于你核心旁边的缓存片中的数据速度很快。访问位于芯片另一端的缓存片则需要穿越片上​​互连​​网络,就像一条微型高速环路。这条路上的每一跳都会增加延迟,既来自路由器逻辑,也来自光速限制下穿越几毫米硅片的简单旅行时间。平均内存访问时间(AMAT)不再是一个简单的层次结构;它取决于你数据的物理位置。

这种非均匀性一直延伸到主内存。在大型服务器系统中,处理器被组织成​​非均匀内存访问(NUMA)​​节点。每个节点都有自己的“本地”内存。一个核心可以相对快速地访问其本地内存。访问连接到另一个节点的“远程”内存则要慢得多,需要通过一个更慢的、处理器间的互连网络。对于性能关键的代码,比如一个高竞争的锁,锁变量的“家”内存是本地的还是远程的,可能会对获取延迟产生巨大差异 [@problem a_id:3625520]。

权衡的艺术

深入多核架构的旅程揭示了它并非一个单一的解决方案,而是一系列精美而复杂的权衡。

  • 我们用原始的单核时钟速度换取并行吞吐量,以保持在我们的功耗预算之内。
  • 我们在诸如​​包容性​​(inclusive)缓存策略和​​非包容性​​(non-inclusive)策略之间做出选择。前者要求共享缓存必须包含所有私有缓存的超集,这简化了一致性,但可能产生额外的无效化流量,而后者则有其自己的一套权衡。
  • 我们演进我们的一致性协议。简单的 MESI 协议有一个性能缺陷:如果一个核心需要从另一个缓存读取一个脏行(dirty line),所有者必须先将其写回内存才能共享它。为了优化这一点,像 ​​MOESI​​ 这样的协议引入了一个​​持有(O)​​状态。该状态允许一个缓存成为脏行的“所有者”,同时与其他读取者共享它,通过快速的缓存到缓存传输直接满足读取请求,并推迟缓慢的写回内存操作。这降低了内存带宽消耗,但代价是更复杂的协议和更大的目录存储。

理解这些原理和机制是释放现代计算机力量的关键。它是透过我们编程语言的抽象,看清机器的本质:一个复杂、逻辑严密、且往往出人意料地优美的核心社会,它们在不断对话,驾驭物理和信息的基本法则来执行我们的命令。

应用与跨学科联系

在深入了解了多核处理器错综复杂的机制之后,我们现在退后一步,审视更广阔的图景。这些原理——并行执行、共享内存、一致性——如何向外辐射,塑造计算世界?要理解这一点,我们必须将多核处理器不视为一个成品,而是一个上演着宏大计算戏剧的舞台。应用是戏剧,而算法和操作系统则是导演,编排着一场复杂的信息之舞,以解决曾经被认为不可能的问题。

这段旅程就像探索一个管弦乐队。理解小提琴或小号如何工作是一回事。完全理解它们如何协同演奏以创作一部交响乐则是另一回事。本章讲述的便是这场交响乐——我们用来协同调度现代处理器众多核心的那些优美而又时而极其复杂的方法。

划分工作的艺术:并行算法

使用核心管弦乐队最直接的方法是给每个核心一段可以独立演奏的乐曲。在计算中,这就是“数据并行”的世界。想象一下,你的任务是在一本城市街区那么大的电话簿中寻找一个名字。一个人可能需要很长时间。但有了一队帮手,你可以简单地把书撕成几部分,每人分一本。第一个找到名字的人喊一声,工作就完成了。

这正是并行化像搜索这样基本算法背后的策略。通过将一个大型、有序的数据数组分割成连续的块,我们可以为每个块分配一个核心。然后每个核心执行自己的本地搜索,第一个找到目标值的核心决定了最终结果。这种方法的美妙之处在于其简单性和可扩展性;对于可以整齐划分的问题,增加更多核心几乎可以带来成比例的速度提升。

但如果问题更像一条装配线,其中一步必须在下一步开始之前完成呢?许多科学和工程领域中最深刻的问题,从天气预报到金融建模,都具有这一特性。你无法在完成今天的计算之前计算明天的天气。在这里,挑战不仅是划分工作,还要理解它的依赖关系。

计算机科学家将这些依赖关系可视化为一个图,其中每个任务是一个节点,如果一个任务必须在另一个任务之前完成,则在两者之间画一条线。然后任务就是将这些任务安排成“波次”或“层次”,并行执行所有相互独立的任务。例如,在执行对科学计算至关重要的某些矩阵变换过程中,一些操作是互斥的,必须按顺序进行,而另一些则是完全独立的,可以同时运行。艺术在于为每个波次找到最大可能的一组独立操作,从而最大限度地提高每个并行步骤完成的工作量。这将一个算法难题变成了一个优美的图论问题,找到最高效的调度方案类似于找到给图着色的最佳方式。总时间则由“关键路径”——最长的依赖任务链——决定,这个概念支配着从建造摩天大楼到计算一个巨大矩阵的逆矩阵的所有事情。

共享的风险:竞争与同步

划分工作只是故事的一半。另一半,通常更困难,是将其重新整合。当多个核心需要访问或更新相同的信息时会发生什么?

想象一条多车道的高速公路——我们的多核处理器——每辆车都需要通过一个单一的收费站。结果是巨大的交通堵塞。这正是许多快速核心试图更新单个共享变量(如全局计数器)时发生的情况。即使是像 count = count + 1 这样的简单操作也会成为瓶颈,因为每个核心都必须排队等待,以便安全地读取、递增和写回值。这种串行化几乎可以完全抵消拥有多个核心的好处。

解决这个问题的一个聪明方法是摆脱单一收费站。相反,我们可以给每条车道配备自己的本地收费员。每个核心维护一个私有的本地计数器,它可以全速更新。只有在周期性地,我们才短暂地停止交通,将所有本地收费员的总数汇总到全局计数器中。这种策略,称为分片或本地聚合,极大地降低了对共享资源的竞争频率,从而带来了巨大的吞吐量提升。

这个例子揭示了并行计算的一个普遍真理:访问共享数据的“交通规则”至关重要。这些规则由同步原语强制执行。考虑两种在具有 MMM 个核心的处理器上管理一组任务的方法。如果我们使用一个“二元信号量”,它就像一个互斥锁或锁,只允许整个系统中一次运行一个任务,我们实际上已将我们的 MMM 车道高速公路变回了单车道公路。加速比为零,多余的核心处于空闲状态。但如果我们使用一个初始化为 MMM 的“计数信号量”,我们实际上是在说“MMM 个任务可以并发运行”。这使得所有核心都能并行工作,实现随核心数量扩展的加速比。一行代码的选择可能就是完全串行化和完美并行化之间的区别。

看不见的指挥家:操作系统与硬件的细微差异

到目前为止,我们一直关注程序员在这场编排中的角色。但在幕后,还有一个不知疲倦地工作的看不见的指挥家:操作系统,它必须做出与处理器底层硬件特性和谐一致的智能决策。

其中最重要的特性之一就是缓存。每个核心都有一个小的、极快的本地内存,即它的缓存,用来存放最近使用的数据。如果核心运行的下一个任务需要相同的数据,几乎可以瞬间检索到,因为缓存是“热”的。一个聪明的操作系统调度器明白这一点。当它看到一个“生产者”线程创建了“消费者”线程将需要的数据时,它会尝试调度消费者在同一核心上紧随生产者之后运行。这最小化了时间间隔,使得数据极有可能仍在核心的私有缓存中,从而避免了缓慢地访问主内存。这种缓存感知调度的舞蹈是软件(操作系统)利用硬件物理现实来获得性能的一个优美例子 [@problemid:3659869]。

硬件的现实可能更加微妙。许多处理器具有“同时多线程”(SMT)技术,通常以 Hyper-Threading 等品牌名称为人所知。这项技术使单个物理核心对操作系统显示为两个(或更多)逻辑核心。这就像让两个职员共享一张桌子和一条电话线。如果一个职员正在打电话(等待来自内存的数据),另一个可以使用桌子(核心的执行单元)。对于许多工作负载来说,这是隐藏延迟和提高利用率的好方法。然而,如果你有一个严重“内存密集型”的任务——意味着它不断需要访问主内存——将两个这样的任务放在 SMT 兄弟线程上可能比将它们放在不同的物理核心上更糟糕。这两个职员现在不断争抢同一条电话线(核心的内存接口),它们的综合性能低于它们各自性能的总和。理解物理核心和逻辑 SMT 核心之间的这种区别对于性能调优至关重要;并非所有的“核心”都是平等的,将任务绑定到适合其工作的正确类型的核心是架构知识的一个关键应用。

扩展管弦乐队:异构计算与未来

现代计算管弦乐队并不仅限于一组相同的 CPU 核心。它是一个异构的乐器组合:用于通用任务的 CPU、用于大规模数据并行的图形处理器(GPU)以及用于自定义逻辑的现场可编程门阵列(FPGA)。我们这个时代的巨大挑战是让这些不同的演奏者协同一致地表演。

这种一致性资源管理的主题一直延伸到芯片本身的设计中。在创建片上系统(SoC)时,硬件工程师面临与软件工程师相同的问题:如何为共享资源(如硬件加速器)提供原子性、互斥的访问。在像 VHDL 这样的硬件描述语言中,他们使用称为 protected types 的构造,其作用与软件中的互斥锁完全相同:它们封装一个共享资源(如一个可用的加速器池),并保证来自不同核心的请求被逐一处理,防止竞争条件。这显示了并发问题的美妙统一性,它在从抽象软件到物理硬件设计的整个堆栈中都有体现。

最激动人心的前沿是相干互连技术的发展,如 Compute Express Link (CXL)。这些是高速通信路径,将处理器的本地“神经系统”——其缓存一致性协议——扩展到外部设备。一个 FPGA,曾经是一个遥远的外设,现在可以连接到 CPU,并几乎被视为一个对等体。它可以从 CPU 的主内存中缓存数据,并且它的修改通过 CPU 核心之间用于通信的相同一致性机制对 CPU 核心可见。这是通过复杂的目录式协议实现的,该协议跟踪哪个核心或设备正在缓存哪块内存,发送有针对性的无效化消息,而不是向系统中的每个人广播。当然,拥有这种能力也意味着巨大的责任。设备和 CPU 都必须遵循严格的排序规则,以确保它们对共享内存的状态达成一致,这是一种内存屏障和门铃写的精妙舞蹈,保证了例如 CPU 只有在加速器可验证地完成写入后才读取结果。

从简单地划分一个列表,到复杂的依赖任务编排,再到操作系统与硬件之间的微妙芭蕾,最后到异构系统的宏伟交响乐,多核计算的旅程是一个永无止境的发现之旅。在这个领域,抽象的数学原理与硅的物理现实相遇,一行代码可以解锁或阻碍十亿晶体管的力量。这个错综复杂、多层次的难题,正是使得駕馭并行性成为现代科学和工程中最具挑战性和最有价值的努力之一的原因。

// Core A data = 42; flag = 1;
// Core B while (flag == 0) { /* spin */ } result = data;