
一个系统“快”是什么意思?虽然我们通常会随意地使用这个词,但在计算机科学和工程领域,“速度”并非单一维度。它是一个由两个关键且往往相互对立的目标——响应性和吞吐量——之间的持续张力所定义的微妙概念。响应性,即低延迟,指的是单个任务能多快完成。吞吐量指的是在给定时间内能完成多少任务。认为改善其中一个会自动改善另一个的普遍直觉,是本文旨在破除的一个根本性误解。要构建真正高效的系统,就必须掌握平衡这两种力量的艺术。
本文将引导您了解这一核心权衡。首先,在“原理与机制”部分,我们将通过类比以及流水线和并行等例子来剖析核心概念,揭示这种二元对立的底层机制。然后,在“应用与跨学科联系”部分,我们将看到这一原理如何体现在从 CPU 架构到云应用的整个技术栈中,从而揭示其作为系统设计普适法则的地位。
在探索世界的过程中,我们常常发现,最深刻的原理往往体现为两种对立力量之间的微妙平衡。在计算与工程领域,这种二元对立中最基本也最美妙的一种,便是响应性与吞吐量之间的权衡。乍一看,它们似乎是同一枚硬币的两面——让事物“更快”难道不是两者都能提升吗?正如我们将看到的,答案是一个引人入胜且响亮的“不”字。要真正掌握构建高效系统的艺术,我们必须明白,优化其中一个往往要以牺牲另一个为代价。
想象一个简单的自动化洗车服务。一辆车进入并经过一系列阶段:预冲洗、泡沫应用、擦洗、最终冲洗和烘干。假设一辆车从湿到干走完所有五个阶段的总时间是 18.5 分钟。这就是它的延迟,或者我们可以称之为端到端的响应性。如果您是那辆车的司机,18.5 分钟是您唯一关心的数字。这是您需要等待的时间。
但如果您是洗车店的老板,您的关注点就不同了。您希望一天内能洗尽可能多的车。您注意到擦洗阶段最慢,需要 5.5 分钟,而其他阶段则更快。一旦第一辆车从擦洗站移动到冲洗站,一辆新车就可以立即进入擦洗站。事实上,一旦整个“流水线”被占满,每当最慢的阶段——擦洗器——完成其任务时,就会有一辆刚洗好的车从最后的烘干机里出来。一辆洗好的车不是每 18.5 分钟出来一辆,而是每 5.5 分钟出来一辆。这个速率——每 5.5 分钟一辆车——就是系统的吞吐量。
悖论的核心就在这里:处理一个项目从开始到结束的时间(延迟)和项目完成的速率(吞吐量)是两个不同的性能衡量标准,由不同的因素决定。延迟由所有任务持续时间的总和决定。而吞吐量,在稳定状态下,完全由系统的瓶颈——链条中最慢的单个阶段——决定。
这种流水线的概念在计算领域被称为流水线技术。这是提高吞-吐量的最强大技术之一。我们可以将一个单一、庞大、复杂的任务分解为一系列更小、顺序的阶段。通过在阶段之间放置“缓冲区”(在数字电路中,这被称为寄存器),我们允许每个阶段同时处理不同的项目。
让我们考虑处理器内部一个数字乘法器的设计。一个乘法运算可以被看作是一系列漫长的逻辑步骤。假设它需要 15.5 纳秒才能完成,那么在没有流水线的情况下,这个乘法器的延迟是 15.5 纳秒,并且每 15.5 纳秒可以产生一个结果。它的吞吐量就是其延迟的倒数。
现在,如果我们巧妙地插入流水线寄存器,将这个漫长的逻辑路径分解为六个更短的阶段,会发生什么?假设这些新的、更短的阶段中最慢的一个现在只需要 5.8 纳秒就能完成。因为所有阶段都在一个主时钟的控制下同步运行,整个流水线现在可以每 5.8 纳秒前进一次。我们每 5.8 纳秒就可以向乘法器输入一对新的数字!吞吐量提升了将近三倍。
但是延迟呢?对于单个乘法运算来说,要通过这个新的流水线,它必须经过所有六个阶段。每个阶段占用一个 5.8 纳秒的时钟周期。该单个操作的总时间现在大约是 。这比原来的延迟增加了一倍多!通过增加流水线寄存器,我们使得单个任务的旅程更长,但我们极大地增加了系统在一段时间内可以处理的总任务数。这就是流水线技术的核心权衡:我们牺牲了单任务的响应性,换来了整体吞吐量的巨大提升。
这个原理不仅限于硬件。一个处理数据流的软件应用可以被构建成一个线程流水线:一个线程读取数据,另一个过滤数据,第三个写入输出。如果这些线程在单个处理器核心上运行,它们仅仅是并发的——它们的执行是交错的,但并非真正同时进行。单个核心是瓶颈,系统的吞吐量受限于处理一个项目所需的总工作量(所有三个阶段处理时间的总和)。但如果我们在各自专用的处理器核心上运行每个线程,我们就实现了真正的并行。现在,这些线程可以像洗车阶段一样同时操作。吞吐量不再受总工作量的限制,而是受最慢线程——即瓶颈——的工作量限制。
流水线技术是一种时间上的并行——在时间上重叠任务。一种更直观的方法是空间上的并行:简单地构建更多的流水线。为什么不并排构建几个相同但更短的流水线,而不是一个深度的流水线呢?。
如果一条流水线每 5.2 纳秒能产生一个结果,那么并行构建三条相同的流水线,不出所料,将在同样的 5.2 纳秒内产生三个结果,使总吞吐量增加三倍。这似乎是增加吞吐量的一种更简单的方法。然而,它在硬件资源(芯片面积、功耗)上付出了巨大的代价。此外,虽然这种方法不像深度流水线那样显著恶化延迟,但用于向并行单元分配工作和收集结果所需的额外逻辑会增加微小的延迟,从而略微增加了任何单个任务的延迟。
无论是加深流水线还是用并行单元拓宽它,目标都是一样的:攻克瓶颈。如果一个 CPU 的性能受限于其访问二级(L2)缓存所需的时间,工程师们面临一个明确的选择。如果 L2 访问需要 ,并且这是最慢的操作,那么整个处理器的时钟周期都受限于这个值。通过对 L2 访问本身进行流水线处理——将其分成两个各为 的阶段——我们可能可以将处理器的时钟周期减半,使其吞吐量几乎翻倍。代价一如既往,是延迟:一次缓存访问过去需要一个(长的)周期,现在需要两个(短的)周期。
但是,我们能无限地增加流水线阶段来提高吞吐量吗?自然,一如既往,施加了限制。流水线操作本身会引入开销。阶段之间的寄存器需要时间来操作,并且时钟信号无法在完全相同的瞬间到达每个寄存器。这些开销创造了一个下限——一个可能的最小-时钟周期——任何程度的流水线都无法克服。超过某个最佳流水线深度后,增加更多阶段只会增加延迟和复杂性,而不会带来任何进一步的吞吐量好处。工程的艺术就在于找到那个最佳平衡点。
到目前为止,我们整个讨论都基于一个关键假设:我们有无穷无尽的独立任务。我们的汽车不需要相互交谈;我们的数据包都是独立的。但当一个任务的结果是下一个任务的输入时,会发生什么呢?这就是数据依赖的诅咒。
想象一个正在对一长串数字求和的程序:total = total + new_value。要执行第 次迭代的加法,处理器必须等待第 次迭代的结果。任务不再是独立的;它们形成了一个依赖链。
在这种情况下,整个游戏规则都变了。考虑两个处理器。处理器 A 有一个快速的浮点单元,延迟为 4 个周期。处理器 B 有两个浮点单元,但每个都较慢,延迟为 6 个周期。
如果我们给它们一连串独立的加法任务(例如,将两个大向量中的数对相加),系统是受吞吐量限制的。处理器 B 凭借其两个单元,每个周期可以完成两次加法,轻松击败处理器 A 的一次。其单元的较高延迟无关紧要,因为总有其他独立的工作可以做。
如果我们给它们有依赖的求和任务,系统是受延迟限制的。处理器 B 的两个单元毫无用处;一次只能有一个工作,因为它必须等待前一个结果。一个新的加法只能每 6 个周期开始一次。处理器 A,尽管硬件只有一半,但速度更快,因为它每 4 个周期就可以开始一个新的加法。
这揭示了一个深刻的真理:计算本身的性质决定了一个系统的性能是由其吞吐量还是延迟所主导。对于高度并行的问-题,我们需要巨大的吞吐量。对于高度顺序、有依赖的问题,低延迟才是王道。现代处理器,凭借其极其复杂的乱序执行引擎,正是这一原则的丰碑。它们拥有大量的并行执行单元以最大化吞吐量。然而,它们在许多真实世界代码上的最终性能往往不是受限于硬件的缺乏,而是受限于程序中最长依赖链的延迟。
那么,如果你的主任务因等待一个长延迟操作而卡住,你昂贵的高吞吐量处理器就只能闲置吗?不一定。这就是现代处理器设计中最聪明的技巧之一:同步多线程(SMT),通常以 Hyper-Threading(超线程技术)的品牌进行营销。
SMT 的核心思想是利用处理器的闲置资源来处理完全不同的事情。当一个执行线程(线程 X)因等待内存数据而停滞时,处理器的指令分派逻辑可以从一个完全不同的线程(线程 Y)中寻找准备就绪的指令,并将它们分派给未使用的执行单元。
结果是又一个美妙的权衡。从线程 X 的角度来看,它的性能略有下降——延迟增加了——因为它现在必须与线程 Y 竞争执行资源。然而,从整个处理器的角度来看,总吞吐量急剧上升。本应闲置的资源现在被有效地利用起来。我们有意地减慢了单个任务的速度,以使整个系统更有效率。对于云服务提供商来说,这是一笔极好的交易:他们可以用相同的硬件为更多的客户服务,只要对任何单个客户的减速保持在可接受的服务水平协议之内。
响应性与吞吐量之间的张力不仅仅是计算机设计中的一个技术注脚;它是一个普适的原则。它适用于工厂车间、软件架构、I/O 系统,甚至我们组织自己工作的方式。通过理解为一个任务提速不同于为多个任务提速,并通过掌握流水线、并行和依赖管理的艺术,我们便能解锁设计出不仅快,而且真正、美妙地高效的系统。
在探索了响应性和吞吐量的基本原理之后,您可能会倾向于认为这种权衡是一个仅限于教科书的、简洁抽象的概念。但事实远比这更激动人心。这一单一而优雅的张力是所有计算领域中最普遍的主题之一。它就像机器中的幽灵,其微弱的低语从处理器的硅芯回响到全球范围的云架构。它是系统设计的一条基本法则,一旦你学会了识别它,你会发现它无处不在。
让我们开启一段穿越现代技术各个层面的旅程,从微观到宏观,见证这一原理的实际应用。不要把它看作是一系列应用列表,而应将其视为一次参观宏伟的画廊,在那里,同一个美妙的思想被以百种不同的风格描绘出来。
我们的旅程始于中央处理单元(CPU)中那个快得不可思议且微观的世界。在这里,决策以纳秒为单位,延迟与吞吐量之间的战斗在其最根本的层面上展开。
想象一下编译器——这位将我们人类可读的代码翻译成机器母语的大师级工匠。编译器面临一个选择。假设它需要计算一个乘法,比如乘以十。它可以使用一个专门的乘法电路,这是一个功能强大但有时速度较慢的硬件。这是直接的方法。但一个聪明的编译器可能知道一个技巧。它知道乘以十与乘以八再加上乘以二的结果是相同的。而对于计算机来说,乘以二的幂次是极其快速的——这只是一个简单的“移位”操作。因此,编译器可以用两个闪电般快速的移位和一个快速的加法来替代一个慢速的乘法。
这里发生了什么?对于单个孤立的计算,关键路径——最长的依赖操作链——现在变短了。结果更快地得出。我们降低了延迟。但代价是什么?我们更多地使用了处理器中较简单的资源——移位器和加法器——而不仅仅是那一个乘法器。如果许多这样的操作同时发生,这个选择可能会为那些较简单的单元造成拥塞。现代处理器的美妙之处在于它们通常可以并行执行这些简单的步骤,这意味着我们有时可以免费获得这种延迟上的好处,而不会损害我们的整体吞-吐量。
当编译器选择使用哪些指令时,同样的情节也在上演。现代 CPU 拥有丰富的词汇,包括一次能做很多事情的复杂指令,比如在一个步骤内计算 的“融合乘加”指令。是使用这个单一、强大的指令更好,还是使用一系列更简单的指令更好?如果你的目标是尽快得到这一个结果,融合指令通常是赢家,因为它减少了延迟。但它可能会在更长的时间内占用处理器中一个高度专业化且稀有的部分。有时,一系列更简单、“更廉价”的指令可以带来更好的整体吞吐量,因为处理器可以更有效地将它们与其他工作进行流水线和交错处理。因此,编译器必须是一个明智的策略师,根据整个程序的上下文来决定是为冲刺(延迟)还是为马拉松(吞吐量)进行优化。
再上一层,我们来到了操作系统(OS)——计算机的总指挥,它 juggling 无数任务并管理所有资源。在这里,权衡不是关于纳秒,而是关于微秒和毫秒,它支配着系统的整体感觉。
想一想你的计算机是如何与网络通信的。网络接口控制器(NIC)是接收来自互联网数据包的硬件。在一个简单的系统中,NIC 可以在每收到一个微小的数据包时就中断 CPU。这将具有极好的响应性;CPU 会立即知道每个数据包的到来。但中断 CPU 的行为本身是昂贵的。这就像一个同事每五秒钟就为了一个微不足道的问题拍你一下肩膀。你将无法完成任何真正的工作!
为了解决这个问题,操作系统使用一种叫做*中断合并*的技术。操作系统告诉 NIC:“不要为每个数据包都来打扰我。等到你有一小批数据包,或者等到一小部分秒过去后,再用整个批次来中断我一次。”结果如何?CPU 被中断的频率大大降低,使其得以解放出来做有用的工作,这极大地增加了它每秒可以处理的数据包数量——这对吞吐量来说是一个巨大的胜利。代价当然是延迟。批次中的前几个数据包必须在 NIC 处等待它们的同伴到达。操作系统必须完美地调整这个等待时间:太长,视频通话会开始卡顿;太短,CPU 则会把时间浪费在扮演秘书的角色上。
这种通过“批处理”来分摊固定成本的原则在操作系统中随处可见。在微内核架构中,像文件系统这样的服务作为独立的进程运行,每次系统调用都需要一次昂贵的上下文切换。操作系统可以鼓励应用程序将它们的请求捆绑成一个更大的进程间通信(IPC)消息,而不是为每个小请求都支付这个成本。同样,当你的应用程序写入数据时,操作系统不一定立即急于将其写入物理磁盘。磁盘很慢。相反,它将写操作收集在一个内存缓冲区(页面缓存)中,并以更大、更高效的块将它们刷新到磁盘。
这引出了一个有趣的控制问题。如果一个应用程序写入数据的速度太快,以至于“脏”(未写入)页面的缓存不受控制地增长怎么办?当系统最终需要释放内存时,它可能会陷入停顿。为了防止这种情况,操作系统使用一个反馈循环:当脏页的比例过高时,它会温和地节流正在写入的应用程序。它故意在短时间内降低它们的吞吐量,以让磁盘赶上进度。这是一个动态的权衡:牺牲一点现在的吞吐量,以防止未来发生灾难性的延迟峰值。这就像一个交通控制系统,控制车辆进入高速公路以防止完全的交通瘫痪。
最后,我们来到了应用层,在这里,这个基本的权衡直接塑造了我们的数字生活。
你有没有想过数据库如何能每秒处理数千个事务?其中的魔力部分在于“组提交”。当你提交一个事务时,数据库必须将一条记录写入到像 SSD 这样的持久性存储设备上的日志中,以保证持久性。在计算机时间里,写入磁盘是一个永恒的过程。如果数据库单独写入每一条日志记录,其吞吐量将惨不忍睹。相反,它会等待几毫秒,收集来自数十个并发事务的日志记录,然后一次性将它们全部写入磁盘。这极大地提高了事务吞吐量。代价是,你的单个事务必须等待这个组集结完成,从而略微增加了它的延迟。
这种为提高效率而进行批处理的思想是高性能计算的命脉,尤其是在人工智能和图形领域。图形处理单元(GPU)是一个并行的野兽,拥有数千个随时待命的核心。但它对于任何新任务都有一个显著的启动开销。向 GPU 发送单个图像进行处理,就像雇佣一个 1000 人的施工队来挂一个相框。为了利用它的能力,我们将数据分组为大批量。在一个计算机视觉流水线中,我们可能会在将数百个相机帧发送到 GPU 之前将它们批处理在一起。在一个大规模的人工智能服务中,我们可能会在加速器上运行推理之前,将用户的请求进行批处理。这就是这些系统如何实现其惊人吞吐量的方式。但权衡总是存在的。对于一辆自动驾驶汽车来说,等待形成一批相机帧所增加的延迟可能是至关重要的。因此,最佳批处理大小并非简单地“越大越好”,而是能够维持所需吞吐量的最小尺寸,因为任何超出这一点的批处理只会增加不必要且可能危险的延迟。
现代流处理系统,实时分析数据,完全是围绕管理这种权衡来构建的。对于一个检测金融欺诈的应用来说,每一毫秒都很重要;它必须以尽可能低的延迟运行。对于一个生成小时分析报告的应用来说,一切都关乎吞吐量;它可以承受将数据缓冲几分钟,以执行更高效、更大规模的计算。
从编译器的指令选择,到操作系统的中断管理,再到数据库的持久性策略,我们看到了同样的原则在重复。我们可以更快地完成事情(低延迟),或者我们可以完成更多的事情(高吞吐量)。伟大的工程艺术和科学在于理解这种权衡,衡量它,并根据手头任务的具体需求来调整它。这是一个美妙、统一的概念,它提醒我们,在计算的世界里,“速度”不是一个单一的数字,而是一个丰富而迷人的选择。