try ai
科普
编辑
分享
反馈
  • 并行性

并行性

SciencePedia玻尔百科
核心要点
  • 并发是在重叠的时间内管理多个任务,而并行是同时执行多个任务,需要多个处理单元。
  • 并行性存在于多个硬件层面,包括指令级并行(ILP)、数据级并行(SIMD)和线程级并行(多核/SMT)。
  • 阿姆达尔定律(Amdahl's Law)指出,并行化所能带来的潜在加速,从根本上受限于程序中必须串行执行的部分。
  • 并行的性能优势常常被各种开销所削弱,例如处理器之间的通信、同步等待以及对共享资源的争用。

引言

在现代计算领域,单处理器速度提升的无情步伐已经放缓,并行性已成为性能提升的主要驱动力。它是一门同时做多件事情的艺术和科学,这个概念支撑着从你口袋里的智能手机到模拟宇宙的超级计算机的一切。然而,释放其力量远非易事。这个领域充满了微妙的区别、基本的限制和隐藏的成本,常常导致人们对真正的并行执行及其幻象感到困惑。

本文将层层剥开这个复杂主题的面纱,以提供一个清晰而全面的理解。我们将从剖析核心原则和机制开始,建立并发与并行之间的关键区别,探索现代处理器内部并行执行的层级结构,并直面那些限制其有效性的无法回避的定律和成本。在这段基础性旅程之后,我们将通过其应用和跨学科联系来探索并行性的变革性影响,看它如何促成优雅的算法、稳健的工程系统和突破性的科学模拟,最终提供一个全新的视角来观察各种复杂的系统。

原则与机制

要真正掌握并行性,我们必须从一个常常引起混淆的区别开始,这有点像分辨两个性格迥异的同卵双胞胎。这对双胞胎名叫​​并发​​(Concurrency)和​​并行​​(Parallelism)。虽然它们看起来很像,并且经常一起出现,但它们代表了根本不同的思想。

幻象与现实:并发 vs. 并行

想象一位厨师在繁忙的厨房里独自工作。这位厨师有多项任务:为沙拉切菜、照看一锅正在煨着的汤,以及为烤肉预热烤箱。在十分钟的时间里,所有三项任务都在取得进展。蔬菜被切好,汤在煨着,烤箱也在升温。这就是​​并发​​:一个系统在重叠的时间段内管理多个任务并使其取得进展。但请注意,在任何一个瞬间,这位厨师只在做一件事——切胡萝卜、搅动汤锅,或者设定烤箱旋钮。同时取得进展的表象是通过在任务之间快速切换所创造的幻觉。

这正是一台只有一个处理核心的计算机所做的事情。它通过给每个程序或线程分配一个极小的时间片(几毫秒),然后迅速转移到下一个,来处理多个程序或线程。这被称为​​时间分片​​(time-slicing)。该系统是并发的,但没有真正的并行性。如果这些任务构成一个流水线,其中一个任务的输出成为下一个任务的输入,那么单个核心必须完成每个阶段的所有工作。处理一个项目的总时间是每个阶段时间的总和,而整体速率,即​​吞吐量​​(throughput),仅仅是这个总时间的倒数。

现在,想象我们雇佣了第二位厨师。一位厨师可以切菜,而另一位可以同时炒菜。这就是​​并行性​​:字面上同时执行多个任务。这需要多个工人——或者在计算机中,多个处理器核心。在一个每个阶段都在自己核心上的并行流水线中,情况就变了。整体吞吐量不再受限于总工作量,而是受限于最慢的阶段——即​​瓶颈​​(bottleneck)。如果过滤阶段耗时最长,那么无论其他阶段有多快,整个流水线生产项目的速度只能和过滤器处理项目的速度一样快。有趣的是,对于通过空流水线的第一个项目,它所花费的总时间(即其​​延迟​​(latency))无论你有一个核心还是多个核心都是相同的。它仍然必须一个接一个地顺序通过每个阶段。并行能帮助你更快地处理一整批项目,但并不一定能加快单个项目的旅程。

并发的故事还有另一个转折。让我们回到只有一个厨师的情况。厨师将烤肉放入预热好的烤箱并设定一个计时器。在接下来的一个小时里,烤箱负责烹饪。在这一个小时里,厨师可以自由地处理沙拉和汤。烹饪与厨师的其他工作是并行发生的,即使只有一个厨师。这个“烘焙”工作被委托给了另一个设备。

这就是现代计算中​​异步I/O​​(输入/输出)的魔力。一个单核上的单线程应用程序可以告诉操作系统:“请从硬盘获取这个文件”,然后立即转去处理其他事件。硬盘控制器——一个独立的硬件——执行读取磁盘这个缓慢的工作。主程序没有被卡住等待;它可以自由地进行其他计算。即使CPU级别的并行性为零,这个系统也表现出高度的并发性,管理着重叠的CPU工作和I/O工作。“进行中”的I/O操作数量成为衡量这种并发深度的一个有意义的指标,这个概念与你有多少CPU核心完全无关。

并行交响曲:执行的多个层次

并行性不仅仅是拥有许多核心。它是一个丰富、分层的概念,是一场在现代计算机中不同规模上同时发生的执行交响曲。

在最微观的层面上,我们发现了​​指令级并行(Instruction-Level Parallelism, ILP)​​。一个现代处理器核心就像一个效率极高的厨师,可以同时完成几个不同的动作——一只手搅拌,另一只手拿香料。即使在执行单个指令线程时,一个​​超标量​​(superscalar)处理器也会预先查看指令流,并找到多个互不依赖的指令。然后,它可以将这些指令分派到不同的内部执行单元,在同一个时钟周期内执行。例如,如果你有100个独立的计算,一个每个周期能执行两条指令的双发射处理器只需50个周期就能完成工作,有效地将时间减半。这是真正的硬件并行性,即使是单个线程也能加速,而且它在处理器架构的深处无形地发生,无需操作系统的任何特殊指令。

再上一层是​​数据级并行(Data-Level Parallelism, DLP)​​。想象一下,我们的厨师有一个特殊的切割器,只需一次按压就能将一整根胡萝卜切成八片。这就是​​SIMD​​(Single Instruction, Multiple Data,单指令多数据)指令背后的思想。程序员可以编写一条指令,比如“add”,CPU会将其同时应用于八对数字。这对于图形处理、科学计算和人工智能来说极为强大,因为在这些领域,你经常需要对庞大的数据数组执行相同的操作。从操作系统调度器的角度来看,这仍然只是一个线程在执行一个指令流;它并不知道每条指令都是一个特洛伊木马,释放出一小支并行的操作军队。

最后,我们来到了最熟悉的层次:​​线程级并行(Thread-Level Parallelism, TLP)​​,它本身也有几种形式。最直接的是拥有多个独立的物理核心——我们的厨师团队。操作系统可以在每个核心上调度一个不同的线程,它们以真正的并行方式运行。但硬件设计师还有另一个锦囊妙计:​​同步多线程(Simultaneous Multithreading, SMT)​​,商业上称为超线程(Hyper-Threading)。SMT允许单个物理核心向操作系统呈现为两个(或更多)逻辑核心。它通过复制核心内部状态的某些部分来实现这一点,使其能够管理两个独立的线程。如果一个线程因等待内存数据而暂时停滞,核心可以立即利用其执行单元来处理另一个线程。这使得来自两个不同线程的指令可以在同一周期内执行。然而,由于这些线程仍然共享许多资源(如主要的计算单元和缓存),它们会相互竞争。结果是,虽然SMT相比非SMT核心提供了真正的性能提升,但其总吞ut量低于两个完全独立的物理核心。

因此,一台现代计算机是嵌套并行的奇迹。在任何给定时刻,它可能在两个不同的核心上执行两个线程(TLP),而每个核心也可能在使用SMT来处理另一个线程;在每个正在运行的线程中,硬件正在寻找独立的指令并行运行(ILP),并执行同时操作多个数据点的SIMD指令(DLP)。

无法回避的现实:并行的局限性

如果并行性如此美妙,为什么我们不能仅仅通过增加越来越多的核心来让我们的计算机无限快呢?现实是,并行性,像所有美好的事物一样,有其局限性和成本。

串行的阴影:阿姆达尔定律

第一个也是最根本的限制是由计算机科学家Gene Amdahl优雅地描述的。​​阿姆达尔定律(Amdahl's Law)​​指出,一个程序通过并行化获得的加速受限于程序中固有串行的部分——那部分根本无法并行运行。想象一下准备一场盛大的宴会。你可以雇佣无数的厨师来并行地切菜和烹饪(可并行的部分,ppp),但所有工作都必须在唯一的主厨最终确定菜单并给出摆盘指令时停止(串行部分,1−p1-p1−p)。无论你雇佣多少厨师,宴会准备好的速度永远不会快于主厨完成这些串行任务所需的时间。

在一台拥有 MMM 个处理器的机器上,理论上的加速比 SSS 由以下著名公式给出: S(M)=1(1−p)+pMS(M) = \frac{1}{(1-p) + \frac{p}{M}}S(M)=(1−p)+Mp​1​ 正如你所见,即使你有无限多的处理器 (M→∞M \to \inftyM→∞),项 p/Mp/Mp/M 也会趋近于零,加速比的上限为 Smax=11−pS_{max} = \frac{1}{1-p}Smax​=1−p1​。如果你的程序中有10%是串行的 (1−p=0.11-p = 0.11−p=0.1),那么即使有一百万个核心,你可能的最大加速比也只有 10×10\times10×。

团队合作的代价:通信与同步

我们的厨师们不能在完全沉默中工作。他们需要沟通和协调。这种开销是对并行性能的一项重大税负。

首先是​​通信开销​​(communication cost)。当处理问题不同部分的处理器需要交换数据时,这些数据必须通过连接它们的导线传输。这需要时间,通常用表达式 α+βm\alpha + \beta mα+βm 来建模,其中 α\alphaα 是一个固定的启动延迟(就像引起某人注意),而 βm\beta mβm 是传输大小为 mmm 的消息所需的时间(对话的长度)。当你为解决一个问题增加更多处理器时,它们通常需要更频繁地相互交谈,这种不断增长的通信时间会开始侵蚀,甚至逆转并行计算带来的好处。

其次是​​同步开销​​(synchronization cost)。线程常常需要相互等待。一个常见的机制是​​屏障​​(barrier)。想象一下,在每道菜准备结束时(一个“阶段”),所有厨師都必须停下来,等到每个人都完成后,才能开始准备下一道菜。这确保了工作流程保持同步。每个阶段的时间由最慢的厨师决定;所有更快的厨师都闲坐着等待。这段等待时间是被浪费的潜力,而屏障本身在程序中充当了一个串行点,阻止了不同阶段工作的重叠。

一个更微妙的同步成本来自​​竞争​​(contention)。想象一下所有厨师都需要使用同一个盐瓶。他们排成一队,一次只能有一个人使用。在计算机中,当多个线程试图访问一个共享资源时,就会发生这种情况,例如内存中一个受​​锁​​(lock)保护的计数器。当一个线程持有锁时,所有其他需要它的线程都会被阻塞。这种强制的串行化创造了一个新的瓶颈,这个瓶颈在单线程版本中是不存在的,从而进一步将实际加速比降低到低于阿姆达尔定律可能预测的水平。

顺序的束缚:数据依赖

有些问题本质上就是串行的。你不能在烤蛋糕之前给它抹上糖霜。在编程中,这被概括为​​数据依赖​​(data dependencies)的概念。考虑计算一个数组的累加和或前缀和的任务:prefix[i] = prefix[i-1] + A[i]。要计算第 iii 个元素的值,你绝对必须拥有第 (i−1)(i-1)(i−1) 个元素的最终值。这是一个距离为1的​​真依赖(流依赖)​​(true (flow) dependence),它创建了一个依赖链,将每次迭代与前一次迭代捆绑在一起。如果你天真地将每次迭代分配给不同的处理器,它们会同时开始,读取到不正确或未初始化的prefix[i-1]值,并产生垃圾结果。要并行化这样的问题,你不能只是堆砌核心;你必须从根本上重构算法本身,使其成为一种更巧妙的形式(如并行扫描算法)。

收益递减法则:盈亏平衡点

最后,即使一个任务是可并行的,也可能不值得这样做。并行化是有入门费的。创建线程、分配工作以及在最后同步它们的开销(kkk 个同步点,每个成本为 csc_scs​)是一项固定成本。如果计算工作的量(nnn 次迭代,每次成本为 ccc_ccc​)很小,这个开销很容易就超过并行执行所节省的时间。存在一个​​盈亏平衡点​​(break-even point),即一个迭代次数的阈值 n∗n^*n∗,低于这个阈值的任务,串行版本更快。只有当问题规模 nnn 足够大,能够摊销固定的并行开销时,并行化才划算。对于小的循环,最快的代码往往是最简单的串行代码。

归根结底,并行性并非万能药。它是一个强大的工具,但需要对问题结构、硬件能力以及协调的微妙成本有深刻的理解。对性能的追求是一场引人入胜的旅程,旨在为正确的问题找到正确的并行级别,并巧妙地平衡同时工作带来的收益与团队合作不可避免的代价。

应用与跨学科联系

在经历了并行性基本原理的旅程之后,我们可能倾向于将其视为计算机架构师的专用工具。但这就像把运动定律仅仅看作是制造时钟的指南。并行性的原则不仅仅是为机器制定的规则;它们深刻地反映了自然界和人类活动中复杂任务是如何完成的。它们提供了一个强大的视角来理解各种系统,从细胞内生命活动的复杂舞蹈到宇宙浩瀚演化的结构。现在,让我们来探索这些原则如何变得鲜活,解决深奥的问题,并连接看似 disparate 的知识领域。

数字宇宙:编排算法

计算的核心是算法,即解决问题的秘诀。其中一些秘诀似乎是自然本身为并行而写的。考虑一下用桥梁连接一组岛屿的最便宜方式——一个寻找最小生成树的经典问题。一个优美的方法,Borůvka算法,其工作方式是让每个岛屿(或岛屿群)独立地识别通往另一个不同岛屿的最便宜的桥梁。在一个宏大的、同步的步骤中,所有这些选定的桥梁都被添加进来。这个过程重复进行,集群不断合并,直到所有岛屿都连接起来。其固有的并行性令人惊叹:每个集群的决策过程都是一个完全独立的任务,允许在最少协调的情况下进行大规模的工作分配。这是我们所谓的“数据并行”的一个完美例子,即相同的操作同时应用于许多不同的数据片段。

然而,并非所有问题都如此慷慨地提供并行执行的机会。更多时候,并行性需要精心的编排。想象一下,试图通过将一个DNA序列与另一个进行比较来揭示其秘密。Needleman-Wunsch算法通过构建一个大的比较得分网格来完成此任务。网格中任何给定单元格的分数取决于其邻居的分数——特别是其左侧、上方和左上角对角线上的单元格。这种依赖关系似乎强加了一个严格的、串行的顺序。然而,一个巧妙的洞察揭示了隐藏的并行性:沿着一条反向对角线的所有单元格可以同时计算,因为它们的依赖关系已经被前一条反向对角线满足了。这创造了一个计算的“波前”,扫过整个网格,使得像GPU这样的大规模并行处理器能够处理否则将无法解决的巨大基因组比较任务。

当我们试图并行化像探索迷宫这样看似简单的事情时,这种依赖之舞变得更加错综复杂——也更能说明问题。这个问题被称为广度优先搜索(BFS)。在并行的BFS中,许多探索者(处理器)从迷宫的某个给定层级开始,同时寻找通往下一层的门。问题在于,当两个探索者同时发现同一个新房间时,谁来认领它?如果两者都认领,这个房间将被浪费地探索两次。如果他们不协调,他们的记录可能会损坏。这是一个“竞争条件”,是共享内存并行中的一个基本危险。解决方案非常精妙:处理器不是对房间的visited状态进行简单的“检查后设置”,而是使用一个原子操作,如比较并交换(Compare-and-Swap, CAS)。这个单一、不可分割的硬件指令确保只有一个处理器能成为第一个将房间状态从“未访问”更改为“已访问”的处理器。晚到一微秒的其他处理器会发现自己来晚了,然后继续前进。这样的原子原语是在繁忙的并行计算城市中防止混乱的纪律严明的交通规则,即使有数百万个交互代理,也能实现正确高效的执行。

工程并行世界:从编译器到数据库

如果没有将这些并行算法付诸实践的工程奇迹,它们的美丽将永远停留在理论中。并行革命的无名英雄之一是编译器——将人类编写的代码翻译成机器指令的软件。一个智能的编译器可以像一个出色的工头一样,自动并行化我们可能没有看作并行的任务。

想象一个程序需要通过为数千个大文件计算校验和来验证它们的完整性。一种简单的方法是一个接一个地处理文件。但是一个现代编译器可以看到一个文件的校验和不依赖于任何其他文件。它可以转换程序,使其在不同的处理器核心上同时处理多个文件。它甚至可以更进一步,创建一个流水线,其中从磁盘读取一个文件(I/O)的慢速过程与为另一个已经读取的文件计算校验和(CPU工作)的快速过程重叠。编译器必须是一个精明的资源管理者,确保这场并行的芭蕾舞不会因过多的文件缓冲区而耗尽系统内存,或因过多的同时读取请求而使I/O子系统不堪重负。这个过程涉及对系统吞吐量和资源约束的仔细分析,平衡待处理数据的到达率与处理器的服务能力。

也许并行性在智力上要求最高的应用是在数据库系统中,这是我们数字社会的基石。在这里,并行性不仅仅关乎速度;它关乎维护秩序。当成千上万的用户同时读写数据库时,系统必须维护一种错觉,即每个用户的事务都是隔离发生的,遵循某种整洁的串行顺序。没有这个称为可串行化(serializability)的保证,混乱就会接踵而至:银行转账时钱可能会消失,或者库存可能被卖两次。

在真正的并行环境中实现这种错觉是一项巨大的挑战。像快照隔离(Snapshot Isolation)这样更简单的方法,即每个事务看到其开始时数据库的一个“快照”,速度很快,但可能在微妙的情况下失败,导致“写偏斜”或“幻读”等异常,即事务应该看到的数据似乎凭空出现或消失。保证真正的可串行化需要更强大——也更优美——的机制。一种方法是严格两阶段锁定(Strict Two-Phase Locking),它使用一个复杂的锁系统,包括不仅能保护单个记录还能保护整个查询范围的“谓词锁”。另一种是可串行化快照隔离(Serializable Snapshot Isolation, SSI),它巧妙地跟踪事务之间的依赖关系,在潜在的因果循环导致不一致状态之前检测并打破它们[@problemžid:3627016]。这些系统是并发控制的杰作,允许大规模并行,同时保持完美的逻辑顺序。

模拟现实:科学的前沿

有了这些强大的算法和系统,人类现在可以应对其最宏大的科学挑战。我们可以在计算机内部构建宇宙,以询问关于现实的“如果……会怎样?”的问题。为了模拟从遥远恒星发出的光穿过星际介质的旅程,天体物理学家求解辐射传输方程。在并行超级计算机上,这变成了一个规模宏大、优雅非凡的问题。模拟可以通过将不同的射线角度分配给不同的处理器组来释放*任务并行。同时,它通过将光在巨大细胞网格上的传播计算为一个“波前”,来使用数据并行*,这与我们DNA比对例子中的波前非常相似。

并行编程的艺术在计算流体力学(Computational Fluid Dynamics, CFD)中达到了顶峰,它模拟从喷气机翼上的气流到超新星的湍流等一切事物。为了求解其中极其复杂的方程,科学家们使用多重网格方法。这个想法非常巧妙:为了修正模拟中的大规模误差,你在一个更粗糙的网格上解决一个简化版的问题,然后将该修正应用回细网格。这种在粗网格上限制和在细网格上延拓的V形循环效率极高。

在一个拥有数千个GPU的系统上并行化一个多重网格V-cycle是在权衡中取得平衡的大师级课程。在细网格上,有足够的工作让所有GPU保持忙碌。但随着算法移向更粗的网格,问题规模呈指数级缩小。在一个微小的问题上保持所有GPU活跃将是荒谬的低效;它们会把所有时间花在通信上,几乎没有时间计算。解决方案是一种聚集(agglomeration)策略:随着网格变粗,工作被聚集到越来越少的GPU上,从而使每个活动的处理器都忙于处理一大块工作。这在V-cycle的下行过程中保持了高效率。然而,这揭示了关于并行扩展的一个深刻事实。最粗网格问题,可能在单个GPU或CPU上解决,变成了一个串行瓶颈。当你为整个问题增加越来越多的处理器(强扩展)时,并行部分变得更快,但花在这个微小的、串行的粗网格求解上的时间保持不变。最终,它主导了总时间,对任何进一步的加速设置了硬性限制——这是阿姆达尔定律在实践中的完美例证。

并行性的边界:当加速遭遇瓶颈

这引导我们到一个关键问题:所有问题都可以并行化吗?答案是否定的。有些问题,由于其本质,是顽固的串行问题。一个绝佳的例子来自我们每天都在使用的东西:数据压缩。Lempel-Ziv(LZ)家族的算法,是ZIP和PNG等流行格式的幕后功臣,其工作原理是用一个简短的反向引用——一个表示“从M字节前回溯复制N字节”的指针——来替换重复的数据序列。

当我们解压这样一个文件时,我们面临着一个依赖链。要解码当前位置的数据,我们可能需要从前一个位置复制数据。但那个先前的数据本身可能是通过一个更早位置的反向引用生成的。在最坏的情况下,可以构造一个文件,其中解码每个新字节都依赖于紧邻其前的字节。这就创建了一个与文件本身一样长的依赖链。用工作-深度模型的语言来说,关键路径长度,或跨度(span)(DDD),与问题大小成正比。由于没有任何并行计算能比其最长的依赖链更快完成,所以无论你投入多少处理器,可实现的加速都是从根本上受限的。

这并不意味着所有希望都已破灭。我们可以做出权衡。我们可以通过将数据切成独立的块并并行解压它们来打破依赖链。但这有代价:通过阻止反向引用跨越块边界,我们可能会错过好的匹配,我们的压缩率会受到影响。这说明了算法设计中最深刻的思想之一:并行度与结果质量之间的权衡。

通用视角:意想不到之处的并行性

并行性的思想——处理器、工作、依赖和通信——是如此基础,以至于它们为我们提供了一个观察世界的新视角,常常是在意想不到的背景下。让我们考虑一个看似无关的现象:分布式拒绝服务(DDoS)攻击,攻击者利用数千台被入侵的计算机或“僵尸网络”向服务器发起流量洪水,使其不堪重负。

我们可以将这次攻击建模为一个并行算法吗?绝对可以。在并行随机存取机(PRAM)的抽象框架中,被入侵的僵尸机就是处理器。算法的总工作量 (WWW)是攻击期间所有僵尸机发送的恶意请求的总数。因为僵尸机大多独立行动,它们之间的依赖链非常短,这意味着跨度 (DDD)很小。这是一种“易并行”计算,其毁灭性的效果来自于工作量与跨度的巨大比率。将一个形式化的计算模型应用于一个对抗性的、真实世界的过程,显示了我们所讨论的原则的普遍力量。理解并行性不仅仅是为了构建更快的计算机;它是为了理解分布式行动的基本性质,无论是建设性的还是破坏性的。

从最小的算法到最大的科学模拟,从数据库的逻辑到网络战争的混乱,并行性的原则提供了一个统一的框架。它们揭示了在一个万物同时发生的世界里,支配工作如何完成、信息如何流动以及秩序如何维持的独立与依赖之间错综复杂的舞蹈。