
多核处理器的出现带来了一个简单而有力的承诺:更多的核心带来更强的性能。这种线性扩展的梦想,即加倍工人数量可将工作时间减半,是并行计算的直观基础。然而,现实远比这更复杂、更有趣。预期的性能增益往往无法实现,撞上无形的墙壁,有时甚至随着核心数量的增加而倒退。这种理论潜力与实践现实之间的差距源于一系列深刻且相互关联的挑战,这些挑战位于现代计算机体系结构的核心。
本文将带领读者探索错综复杂的多核性能世界,全面概述了决定性能的各种因素。我们将首先探讨奠定根本限制的基础“原理与机制”。本节将介绍 Amdahl 定律、Roofline 模型所描述的内存带宽限制、同步的隐藏成本以及功耗墙这一终极物理障碍等概念。随后,“应用与跨学科联系”一节将阐释这些原理在现实世界中的具体表现。我们将看到这些理论概念如何在操作系统调度器、网络协议栈、科学模拟和视频游戏引擎等实际场景中显现,揭示这些性能挑战的普遍性。
多核处理器是现代世界的引擎,是一项工程奇迹,它承诺了一个简单而诱人的想法:如果一个人挖一条沟需要十小时,那么十个人就能在一小时内完成。如果一个核心解决一个问题需要一小时,那么十六个核心肯定能在四分钟内解决?这就是完美的线性扩展之梦。和许多美好的梦想一样,它与一系列严酷、迷人且极具启发性的现实发生了碰撞。理解多核性能的旅程,就是一次深入复杂性核心的旅程,在这里,简单的加法会导致非线性的后果,而最大的挑战往往是系统各部分之间那些看不见的相互作用。
我们的旅程始于最根本的限制,一个如此强大,以至于能支配任何需要各部分协同工作的系统的原则。它被称为 Amdahl 定律。想象一下,你正在准备一场盛大的宴会。你可以雇佣一支厨师大军并行地切菜、搅锅和摆盘。但无论你有多少厨师,他们都必须等待那台唯一的、主烤箱来烘烤宴会的主菜——烤肉。等待烤肉的时间就是你烹饪过程中的串行部分。
每个计算机程序,无论多么复杂,都有这样的串行部分——代码的一部分由于某种原因必须由单个工作单元顺序执行。这可能是初始化数据、最终确定结果,或者是一个逻辑上不可分割的部分。我们假设这个串行部分为 。剩下的部分,,是可并行的部分。
如果单个“轻量级”核心能以 的速率执行指令,那么运行一个包含 条指令的工作负载所需的总时间是 。使用 个这样的核心,我们可以将并行部分的速度提高 倍。然而,串行部分花费的时间不变。新的总执行时间变为:
总吞吐量,或有效指令速率,即为 ,简化后得到:
这个方程以多种形式出现,用于为并行系统建模,它是一个简单真理的数学体现。当你增加越来越多的核心(即 变得非常大), 这一项会消失。吞吐量 并不会趋向无穷大,而是被卡在 的上限。如果你的程序仅有 是串行的(),那么你可能的最大加速比就是 倍,即使你拥有一千个核心!这条定律是我们第一个,也是最发人深省的现实检验。一个问题中无法被分割的部分,最终主宰了它的命运。
让我们想象一下,我们找到了一个完全可并行化、串行部分几乎为零的问题。我们绕过了 Amdahl 定律。我们就自由了吗?完全没有。我们只是用一个问题换了另一个问题。如果所有厨师都堵在一个小小的储藏室门口,那么再多的厨师也毫无用处。对于处理器来说,那个储藏室就是主内存。
现代核心是贪婪的性能猛兽,每秒能够执行数十亿条指令。但这些指令需要数据。性能不仅仅在于你计算的速度有多快,还在于你给计算器“喂”数据的速度有多快。这种二元性被优美的 Roofline 模型 所捕捉。想象一下房子的屋顶。它的高度是你系统的峰值性能。但这个屋顶有两个斜面:一个由你核心的峰值计算速率决定(它们能做多少 GFLOP/s),另一个由你的内存带宽决定(你每秒能移动多少 GB/s 的数据)。你的实际性能总是被困在这个屋顶之下。如果你的任务每次计算都需要大量数据(即“算术强度”低),你的性能就会沿着内存带宽的斜面滑落,远低于处理器的计算峰值。你就变得受内存限制了。
在一个受内存限制的问题上增加更多核心,就像在超市增加更多收银台,却不雇佣更多人来补货。一开始,速度会变快。但很快,所有收银员都在等待商品。一旦系统的总内存带宽耗尽,加速效果就会饱和。此时,增加更多核心不会带来任何提升;它们只是加入了等待数据的工人队列。
但就在这个限制的核心地带,存在着一种极其美妙的现象:超线性加速。假设你有一个单核心正在处理一个非常大的经济模型。模型的数据(其“工作集”)太大,无法装入核心的小型、闪电般快速的本地缓存中。核心必须不断地从缓慢、遥远的主内存中获取数据,就像一个木匠每需要一颗钉子都得走到木材厂去取。这个核心大部分时间都在等待,而不是工作。
现在,让我们把问题分散到,比如说,八个核心上。总问题规模不变,但每个核心现在只负责八分之一的数据。奇迹就在这里发生:如果这块较小的问题现在能完全装入每个核心的私有缓存中,奇妙的事情就会发生。在初始“预热”加载数据之后,每个核心都发现它需要的每一颗钉子都在它的工具带里。那些持续而缓慢的主内存访问消失了。每个核心的效率都变得比原来单个核心高得多。结果呢?总加速比可能超过八倍。这就好像雇佣了八个木匠,并给每个人一小部分工作,结果每个人都自发地学会了以两倍的速度工作。这不是对物理学的违背;这是内存层级结构非线性特性的一个绝妙结果。
到目前为止,我们一直想象我们的核心在各自的数据块上出色地独立工作。但现实世界很少如此整洁。并行任务常常需要协调、共享信息、访问公共资源。而只要有共享,就有冲突的可能。
想象一个一次只能由一个核心使用的资源——一个共享计数器、一条数据库记录,或者一个像专用乘法器这样的硬件单元。为了管理这个资源,我们使用锁 (lock) 或互斥锁 (mutex)(mutual exclusion 的缩写)。它就像一个数字版的“发言权杖”;只有持有它的核心才被允许访问该资源。
这种为获取锁而必须等待的行为被称为同步开销,它的作用就像串行部分一样,根据 Amdahl 定律限制了我们的加速比。但现实可能比这个简单模型所揭示的要糟糕得多。锁与操作系统调度器之间的交互可能产生病态行为。其中最臭名昭著的就是护航效应 (convoy effect)。
想象一条高速公路上的单车道收费站。一辆车(线程 A)支付了过路费(释放了一个锁)并可以自由离开。但它没有加速,而是决定停在刚过收费站的地方查看 GPS(运行其非关键性工作),占用了唯一的车道。在它后面,一长串汽车(其他线程)正在等待。队伍中的下一辆车(线程 B)已经准备好了钱,收费站技术上是空闲的,但它无法前进,因为线程 A 堵住了路。这就是一个护航队。系统的吞吐量之所以停滞不前,不是因为共享资源(收费站)繁忙,而是因为资源使用者与道路本身(CPU 核心)之间发生了不幸的交互。
最微妙的冲突源于使缓存如此强大的机制本身:它们的私有性。每个核心都有自己的私有缓存,用于存放数据的本地副本。但如果核心 0 和核心 1 同时拥有同一份数据的副本,而核心 0 修改了它,会发生什么?核心 1 现在看到的就是过时的、不正确的数据。
为了防止这种情况,处理器实现了一种缓存一致性协议,比如常见的 MESI(已修改-独占-共享-无效)协议。这是一个缓存之间的后台通信系统,通过低语和呐喊来确保每个人对内存的视图保持一致。当一个核心写入一块数据时,它必须使所有其他副本失效,迫使其他核心在需要时重新获取新数据。
这个过程可能导致一个奇异而令人沮丧的问题,称为伪共享 (false sharing)。数据在主内存和缓存之间移动不是逐字节进行的,而是以称为缓存行 (cache lines)(通常为 64 字节)的固定大小块进行的。想象两个变量 X 和 Y,它们彼此毫无关系。核心 0 始终只读写 X,核心 1 始终只读写 Y。它们在执行完全独立的任务。但纯属运气不好,X 和 Y 恰好在内存中相邻,并落在了同一个缓存行上。
现在,当核心 0 写入 X 时,一致性协议并不知道它只改变了 X。它会大喊:“整个缓存行都被修改了!”并使核心 1 的副本失效。片刻之后,核心 1 写入 Y。它反过来又使核心 0 的副本失效。这个物理缓存行在两个核心之间激烈地“乒乓”传递,每一次传输都会产生显著的延迟损失。这两个核心,虽然逻辑上是独立的,却陷入了一场性能杀戮的战争,争夺一个它们甚至没有意识到正在共享的资源。
让我们假设我们是杰出的程序员,已经解决了所有这些问题。我们有一个无限可并行的算法,没有内存瓶颈,也没有竞争。现在我们能构建一个拥有一百万个核心的芯片,并实现百万倍的加速吗?不能。我们撞上了最后、也是最坚硬的一堵墙:功耗。
曾经带给我们 Dennard 缩放(即更小、更快的晶体管也消耗更少的能量)这一馈赠的晶体管物理学,已经背叛了我们。如今,缩小晶体管不再能带来同样的功耗效益。其后果是严峻的:我们可以制造拥有数十亿晶体管的芯片,但我们无法承受将它们全部同时开启的代价,否则芯片会熔化。这就是暗硅 (dark silicon) 问题。我们的芯片就像一个拥有百万房屋的城市,但我们只有足够的电力来点亮其中几个街区。
这种限制迫使架构师做出有趣的权衡。哪种更好:几个大型、复杂、高功耗的“重量级”核心,还是一支由小型、简单、高能效的“轻量级”核心组成的大军?[@problem_-id:3630874] 如果你的工作负载有很大的串行部分,那么强大的重量级核心是加速通过该瓶颈的最佳选择。如果它是高度并行的,那么轻量级核心大军可能会胜出。
这种选择甚至延伸到我们如何操作那些我们能够开启的核心。我们可以让少数核心以“睿频模式”运行——高频率、高电压、高性能,但功耗巨大。或者我们可以让更多核心以“节能模式”运行——速度较慢,但能效高得多。哪种配置是“最佳”的?答案完全取决于你的目标。你是为了纯粹的速度(最短执行时间,)进行优化?还是为了能源效率(最低能耗,)进行优化?像能量延迟积 (EDP)(即 )或能量延迟平方积 ()(即 )这样的指标,为我们提供了一种数学语言来表达这种权衡。为 进行优化会更严厉地惩罚延迟,将你推向高功耗、高速的睿频配置。而为 EDP 进行优化则会偏爱更均衡、更节能的模式。没有唯一的“最佳”答案,只有针对特定目标的最佳答案。
多核性能的世界不是一个凭蛮力就能征服的世界。它是一个由物理和逻辑约束构成的迷宫。理解其原理是航行的艺术。它使我们能够做出明智的选择,以平衡相互竞争的目标。
考虑一下保护共享资源时在自旋锁 (spinlock) 和互斥锁 (mutex) 之间的经典选择。自旋锁会忙等待:它在一个紧凑的循环中不断检查锁,消耗 CPU 周期。互斥锁则更有礼貌:如果锁被占用,它会将 CPU 让给操作系统,并请求在锁被释放时被唤醒。互斥锁避免了浪费 CPU 时间,但让出和被唤醒的行为会产生巨大的开销(一次上下文切换)。哪个更好?如果你知道锁被持有的时间非常短,并且有空闲的核心,那么自旋会更快。你获得锁的时间将少于操作系统让你休眠和唤醒你的时间。但在一个超额订阅的系统上,或者在只有一个核心的系统上,自旋是一场灾难——你正在浪费锁持有者完成工作并释放锁所需要的资源。
同样的经济学思维也适用于硬件层面。在固定的预算下,架构师应该增加更多核心,还是用这些资金(和硅片面积)来构建一个更大的 L3 缓存? 答案取决于工作负载。性能会从更好的并行化中获益更多,还是从减少内存停顿中获益更多?
从线性扩展的简单梦想,到现代处理器的复杂现实,这段旅程揭示了性能的真正本质。它不是一个单一的数字,而是系统的一种涌现属性,是在计算、内存、通信和功耗之间达成的微妙平衡。释放这种性能是一个持续发现的过程,由对这些优美而复杂机制的理解所引导。
在领略了支配并行计算之舞的原理与机制之后,我们现在走出抽象,进入现实世界。在这里,优雅的并行定律与实际机器的混乱而奇妙的复杂性,以及我们要求它们解决的多样化问题相遇。只知道游戏规则是一回事;亲眼看到它们在赛场上如何展现——在操作系统中,在科学发现中,在我们屏幕上的游戏中——则是另一回事。这正是该主题真正美妙之处的显现,它不是一堆孤立事实的集合,而是一套统一的原则,其影响遍及现代技术的每一个角落。
就像物理学家认识到支配苹果下落的万有引力定律同样维系着行星的轨道一样,我们将会看到,加速视频游戏的并发性和数据局部性原理,同样是操作系统设计和超级计算机巨大能力的核心。我们的探索将不仅仅是应用的罗列,而是一次见微知著、发现普遍规律的旅程。
每个学习并行计算的学生首先都会学到加速比的基本限制,这是一个如此简单而强大,感觉就像自然法则的定律。如果一个程序中有一部分是顽固的、内生性的串行——即完全无法并行运行的部分——那么无论你投入多少处理器,这部分最终都会限制你所能实现的最大加速比。如果你的任务仅有 10% 是串行的,你永远、永远无法获得超过十倍的加速。这就是 Amdahl 定律的精髓。我们可以在机器人导航这样的任务中清晰地看到这一点,机器人必须在构建其周围环境地图的同时,确定自己在该地图中的位置(一个称为 SLAM 的过程)。虽然这项任务的部分工作,如处理传感器数据,可以完美地分配给多个核心,但最后整合地图的步骤——闭合回路并意识到你回到了起点——通常是一个串行瓶颈。即使有八个核心,计算中顽固的串行部分也将整体加速比限制在一个更为温和的数字上,可能只有两到三倍。
这一定律设定了我们期望的极限。但当我们试图驶向那个极限却发现自己在倒退时,会发生什么?例如,一个计算化学专业的学生可能正在使用密度泛函理论来运行一个分子的复杂模拟。为了急于得到结果,他们将处理器核心数从八个增加到十六个,却发现计算现在需要更长的时间。这艘船不仅仅是达到了速度极限;它似乎正在进水。这是怎么回事?Amdahl 定律被打破了吗?
完全没有。Amdahl 定律描述的是一个没有摩擦和开销的理想世界。我们的世界并非如此纯净。学生看到的“负向扩展”揭示了一个更深层次的真相:增加更多工人不是免费的。实际上,协调他们的成本有时会超过他们劳动带来的好处。这个令人费解的结果是理解多核性能真正挑战的入口。它迫使我们深入探究机器作为物理现实的本质。
造成这种减速的罪魁祸首是我们之前见过的一群角色,但现在我们看到它们在实际行动中:
这一个例子表明,扩展性能不仅仅关乎算法;它关乎将机器理解为一个资源有限的物理系统。
让我们聚焦于内存系统,这个上演了如此多性能大戏的舞台。想象一个现代视频游戏引擎,它必须在每一帧更新数千个物体的位置和速度。一种常见的设计,即实体-组件系统(Entity-Component System),将所有位置存储在一个大数组中,所有速度存储在另一个数组中。为了利用所有核心,引擎以交错的方式分配线程来更新不同的物体:线程 0 处理物体 0、4、8,...;线程 1 处理物体 1、5、9,...;以此类推。
从逻辑上看,这些线程正在处理完全独立的数据。但从物理上看,它们正在玩一场灾难性的缓存行乒乓游戏。一个缓存行,即 CPU 处理的最小内存块,可能存放着物体 0 到 4 的位置。当线程 0 写入物体 0 时,它将该缓存行拉到自己的核心。一瞬间之后,当线程 1 写入物体 1 时,系统必须使第一个核心的副本失效,并将整个缓存行拉到第二个核心。线程 2 和 3 也做同样的事情。这种现象,即线程虽然访问其中的不同数据却在争夺同一个缓存行,被称为伪共享 (false sharing)。解决方案要么是改变舞蹈(将线程分配给连续的对象块,使它们在不同的缓存区域工作),要么是改变舞池(填充数据,使每个物体的位置都位于自己的私有缓存行中)。
这是一个微妙但至关重要的洞见:在多核世界中,不存在真正独立的内存访问。你的数据的邻居很重要。
虽然伪共享是关于意外的碰撞,但有时碰撞是刻意为之的。考虑一个操作系统中的简单引用计数器,内存中的一个数字,用于跟踪系统中有多少部分正在使用一个共享对象。每当建立一个新的引用时,一个核心就必须原子地增加这个数字。在一个拥有许多核心的系统上,这个单一的共享计数器成为一个普遍的瓶颈。每个想更新它的核心都必须排队,等待轮到自己来获得对该内存位置的独占访问权。整个多核机器的威力都被序列化通过这根针的针眼。
解决方案既优雅又实用:停止要求完美的、即时的知识。每个核心维护自己的、私有的本地计数器,而不是一个全局计数器。它可以廉价而快速地增加其本地计数器。只有在周期性地,它才会获取全局计数器的锁,将自己的本地总数以单次、批量更新的方式加进去。在短暂的时间内,全局计数是“陈旧”或错误的,但系统的吞吐量却提高了几个数量级。我们用一点点的准确性换取了巨大的性能增益,这种权衡是可扩展系统设计的核心。
内存之舞可能更加微妙。为我们提供虚拟内存便利的机制本身——即每个进程都相信自己拥有私有的地址空间——也带来了多核成本。从虚拟地址到物理地址的映射被缓存在每个核心的转译后备缓冲区(TLB)中。如果操作系统为了安全或内存管理而更改了一个映射,它必须执行一次“TLB 击落 (TLB shootdown)”,向所有其他核心发送一个中断,告诉它们使其旧的、过时的翻译失效。这个过程是一个全系统的停顿,是对性能的一种隐藏税收,随着核心数量和被重新映射内存区域大小的增加而增长。对于依赖通过共享内存进行高速进程间通信 (IPC) 的应用来说,这种操作系统级别的开销可以直接限制可实现的消息速率。
如果说性能是一个管弦乐团,那么操作系统就是它的指挥。操作系统不仅仅是另一个应用程序;它是负责管理硬件、调度工作、并为所有其他应用程序创造运行环境的实体。它每秒做出数千次的决策,决定了结果是交响乐还是嘈杂声。
考虑一下高速网络的挑战。一个现代网卡每秒可以向服务器涌入数百万个数据包。一个未经优化的系统可能会让一个核心处理来自网卡的硬件中断,另一个核心处理网络协议,还有一个核心运行等待数据的应用程序。每一次交接都涉及跨核心通信和潜在的缓存未命中,因为数据包的数据从一个核心的缓存被推送到另一个核心。
网络调优的艺术在于创建一个“完美亲和性”的流水线。通过结合硬件特性(如可以将数据包导向不同硬件队列的接收端缩放 (Receive Side Scaling, RSS))和软件设置(如 IRQ 亲和性和接收数据包引导 (Receive Packet Steering, RPS)),一位熟练的工程师可以确保一个数据包,从它到达网卡的那一刻起,到它的数据被应用程序消费的那一刻,都由同一个核心处理。这为数据创建了一条快车道,最大化了缓存局部性,并消除了调节跨核心交接的处理器间中断 (IPIs) 的开销。结果是网络吞吐量的大幅提升和延迟的降低。
这种平衡竞争目标的主题是调度器设计的核心。想象一个由频繁因 I/O 而阻塞的线程组成的工作负载。硬亲和性策略将每个线程固定到特定的核心上,这对于缓存局部性非常好。但是,如果一个核心上的两个线程碰巧都阻塞了,那么该核心就会闲置,浪费资源,而其他核心可能有一长串的工作队列。吞吐量和公平性都会受到影响。
软亲和性策略使用周期性的负载均衡将线程从繁忙的核心迁移到空闲的核心。这通过保持所有核心繁忙来提高吞吐量,并通过给所有线程运行机会来增强公平性。但这也有代价:调度器本身消耗 CPU 时间来做出这些决策,而且每当一个线程迁移时,它都会遭受“缓存预热”的惩罚,因为它需要重新填充新核心上的缓存。存在一个最佳点——一个“金发姑娘”般的平衡频率,它既足够快以防止核心空闲,又不会快到让平衡和迁移的开销超过其带来的好处。
当我们把功耗也考虑进来时,这种平衡行为变得更加复杂。为了满足严格的延迟服务水平协议 (SLA),我们的直觉可能会告诉我们将任务分散到所有可用的核心上,让系统以最大的并行度来处理工作负载。拉取迁移策略,即空闲核心主动“窃取”工作,可以实现这一点,从而最小化排队延迟。然而,为了节省能源,另一种策略更好:将所有任务整合到少数几个核心上,并让其余核心进入深度睡眠状态。推送迁移策略可以主动地将任务打包在一起以实现这种整合。利用排队论的数学工具,我们可以量化这些选择。我们可能会发现,以低功耗频率分散任务可以满足我们的延迟目标,而整合任务则不能。通过切换到高性能频率,也许两种策略都变得可行,但分散任务仍然以更高的功耗为代价提供更低的延迟。没有单一的“最佳”答案;只有针对一组给定目标——速度、效率或响应性——的最佳答案。
从 Amdahl 定律的基本限制到操作系统调度器所做的错综复杂的现实权衡,我们看到了一个贯穿始终的主题。多核性能是系统的一个整体属性。它源于算法、数据结构、编译器、操作系统和硅的物理现实之间的相互作用。要掌握它,就要欣赏这些层次之间的深刻联系,并学会为了追求一个目标而平衡它们的艺术。