try ai
科普
编辑
分享
反馈
  • 内存延迟:现代计算中无情的时钟

内存延迟:现代计算中无情的时钟

SciencePedia玻尔百科
核心要点
  • 内存层级结构利用概率和多层缓存来掩盖主存的高延迟,这一原则通过平均内存访问时间(AMAT)进行量化。
  • 通过TLB和页表进行的地址翻译是一个关键的性能瓶颈,其未命中会导致缓慢的页表遍历或毫秒级的页错误。
  • 多核性能既取决于NUMA系统中的内存地理分布,也取决于通过精心的数据布局来避免伪共享等缓存一致性陷阱。
  • 虚拟化和无服务器计算等抽象概念带来了新的延迟挑战,从业界嵌套的页表遍历到由页错误引起的冷启动延迟。

引言

在现代计算中,处理器巨大的速度常常受限于一个单一且无情的约束:从内存中检索数据所需的时间。这个延迟,即内存延迟,不仅仅是一个技术规格,更是一种塑造了计算机体系结构和软件性能全景的基础力量。虽然我们直观地理解越快越好,但内存延迟的真实性质远比一个单一的数字复杂。它是一个概率性的、多方面的过程,访问时间从纳秒到毫秒不等,取决于系统内存层级结构内一连串的事件。对于任何希望编写高效软件或设计高性能系统的人来说,理解这一过程至关重要。

本文对内存延迟进行了全面的探讨。我们将首先剖析其核心的原理与机制,从内存层级结构、平均内存访问时间(AMAT)的概念,以及通过TLB和页表进行的复杂虚拟地址翻译过程开始。我们将揭示由页错误和上下文切换等事件引起的剧烈性能悬崖。随后,应用与跨学科联系一章将阐述这些原理如何在不同领域中体现。我们将看到处理器架构师、软件开发者和操作系统设计者如何与延迟进行持续的斗争,以及多核NUMA系统、虚拟化和无服务器计算等现代范式如何引入其独特的挑战与解决方案。通过从芯片级的机制到云的广阔规模的探索,您将深刻体会到那些巧妙的“欺骗”与权衡,正是它们让我们的系统在内存访问这把无情时钟的统治下,能够以如此之高的速度运行。

原理与机制

要理解现代计算机,你必须理解一个基本的权衡:我们无法构建一种同时具备海量、闪电般快速和廉价特性的内存。如果可以,计算机体系结构会简单得多。因此,我们被迫变得聪明。我们构建了一个​​内存层级结构​​(memory hierarchy),一个由不同种类内存组成的金字字塔,每一层都比其下一层更小、更快、也更昂贵。在顶端,最接近处理器的是微小而极速的缓存。在底部是容量巨大但相对迟缓的主存,甚至是更慢的磁盘存储。整个性能工程的艺术都寄托在一个简单的希望上:处理器此刻需要的数据正等待在这个层级结构中最快、最近的一层。因此,访问内存不是一个单一、确定的行为;它是一段旅程,一场关于概率和时间的游戏。

内存层级结构:一场概率与时间的游戏

想象一下你需要一条信息。你的第一站是一级(L1)缓存,即处理器的个人记事本。访问它快得令人难以置信,也许只需要一纳秒(111 ns)。在大多数情况下,比如90%90\%90%的时间里,你需要的东西就在那里。这就是​​缓存命中​​(cache hit)。但另外10%10\%10%的时间呢?这就是​​缓存未命中​​(cache miss),你的旅程必须继续。

发生未命中时,你不会放弃;你会前往下一级,即二级(L2)缓存。它更大,但稍慢一些。到达那里可能需要额外花费555 ns。也许你有80%80\%80%的机会在这里找到你的数据(在那些L1未命中的请求中)。如果再次未命中,你会继续前往三级(L3)缓存,它更大也更慢,可能需要再花费151515 ns。最后,如果每一级缓存都未命中,你必须踏上前往主存的漫长而艰辛的旅程,这趟旅程可能需要100100100 ns甚至更长时间。

那么,你在这段旅程中花费的平均时间是多少?我们可以计算出来,并且通过计算,我们将揭示计算机性能中最基本的公式之一:​​平均内存访问时间(AMAT)​​。AMAT是我们旅程的期望时间,是所有可能结果的加权平均值。

让我们追踪这条路径。每一次访问,无一例外,都需要我们检查L1缓存,所以我们总是要支付L1的命中时间(thit,1t_{\mathrm{hit},1}thit,1​)。如果我们在L1中未命中(其发生概率等于未命中率m1m_1m1​),我们就必须支付L2的命中时间(thit,2t_{\mathrm{hit},2}thit,2​)来检查那一级。如果我们同时在L1和L2中未命中(这一事件的概率为m1×m2m_1 \times m_2m1​×m2​,其中m2m_2m2​是对于已经未命中L1的访问而言L2的未命中率),我们就要支付L3的命中时间(thit,3t_{\mathrm{hit},3}thit,3​)。而如果我们处处未命中(概率为m1m2m3m_1 m_2 m_3m1​m2​m3​),我们最终要支付主存的延迟(tMt_MtM​)。将这些概率成本相加,便得到总的AMAT:

AMAT=thit,1+m1thit,2+m1m2thit,3+m1m2m3tM\mathrm{AMAT} = t_{\mathrm{hit},1} + m_1 t_{\mathrm{hit},2} + m_1 m_2 t_{\mathrm{hit},3} + m_1 m_2 m_3 t_MAMAT=thit,1​+m1​thit,2​+m1​m2​thit,3​+m1​m2​m3​tM​

这个优雅的公式讲述了一个故事。总平均时间是旅程中每个阶段所花费时间的总和,每个后续阶段的成本都因必须走那么远的概率递减而被打了折扣。使用一个假设系统的一些典型数值,比如111 ns的L1、555 ns的L2、151515 ns的L3和100100100 ns的主存,以及合理的未命中率,我们可能会发现AMAT仅为2.42.42.4 ns。这就是层级结构的魔力:一个最终依赖于缓慢的100100100 ns内存的系统,平均而言,给人的感觉就像运行在快了近40倍的内存上一样。这是概率设计的胜利。

地址簿:从虚拟梦想到物理现实

故事变得更加错综复杂。你的程序使用的内存地址并非真实的;它们是​​虚拟地址​​。你的程序存在于一个干净、私有、连续的地址空间中,这是一个由操作系统创造的幻想世界。而计算机的物理内存,则是一种有限、共享且通常是碎片化的资源。连接这个虚拟梦想与物理现实的桥梁是一组称为​​页表​​的数据结构,它们充当了系统的地址簿。

要访问任何数据,处理器必须首先将虚拟地址翻译成物理地址。如果每次指令执行都必须查阅驻留在主存中的页表,性能将会陷入停顿。一次访问就需要多次准备性访问!为避免这种情况,CPU使用一个专门用于翻译的、速度极快的特殊缓存:​​转译旁观缓冲器(TLB)​​。TLB就像你桌上写着最常用电话号码的便签;它比每次都在电话簿里查找要快得多。

TLB命中是美妙的——翻译在一两个周期内就准备好了。但TLB未命中会迫使硬件执行一次​​页表遍历​​(page table walk):在页表这个“地址簿”中进行一次缓慢而有条不紊的查找。由于页表本身为了节省空间也是层级化的,这次遍历涉及一连串相互依赖的内存读取。要找到第ddd级的条目,你必须先读取第d−1d-1d−1级的条目。如果这ddd次查找中的每一次都未命中数据缓存而不得不访问主存,每次耗费LLL个周期,那么页表遍历的总代价就是cptw=d×Lc_{\text{ptw}} = d \times Lcptw​=d×L个周期。这揭示了一个深刻的联系:一个软件数据结构的设计(一个ddd级页表)直接创造了一个硬件的性能特征。

很自然地,如果一个缓存是好的,那么两个会不会更好?就像数据缓存一样,我们也可以有一个TLB的层级结构。一个L1 TLB提供最快的翻译。在未命中时,我们可以在诉诸昂贵的页表遍历之前,检查一个更大、稍慢的L2 TLB。这引入了一个权衡。L2 TLB的查找为每次L1未命中增加了一个小的固定成本,但它提供了避免完整遍历所带来的更大代价的机会。只有当L2 TLB在捕获未命中方面“足够好”时,两级系统才更优。具体来说,L2 TLB的条件命中率(h2h_2h2​)必须大于其自身查找成本(c2c_2c2​)与页表遍历代价(www)之比。即,h2>c2wh_2 > \frac{c_2}{w}h2​>wc2​​。这个简单的不等式体现了一个核心的工程原则:一项优化只有在其收益超过其成本时才是有价值的。

驾驭性能悬崖

到目前为止,我们一直在纳秒的世界里旅行。但在这一景观中存在着悬崖——一些事件可以突然将一次访问的成本从纳秒 catapult 到毫秒,这是一个百万倍的增长。其中最剧烈的就是​​页错误​​(page fault)。

当页表告诉处理器所请求的数据根本不在物理内存中时,就会发生页错误;它已被临时移动到速度慢得多的磁盘上。处理一个页错误是一项史诗般的任务。操作系统必须停止该程序,找到一个空闲的物理内存帧,向磁盘发出命令,等待机械设备找到并传输数据(这个过程需要数百万纳秒),更新页表,最后恢复程序的执行。

其成本是如此巨大,以至于扭曲了我们对平均值的感知。想象一个系统,其中一次正常访问(包括TLB命中和未命中)大约需要52.552.552.5 ns,而一次页错误需要7.57.57.5 ms。在什么样的错误率下,平均访问时间会翻倍?惊人的答案是,仅仅pf∗=52.5 ns7.5×106 ns≈7×10−6p_f^* = \frac{52.5 \text{ ns}}{7.5 \times 10^6 \text{ ns}} \approx 7 \times 10^{-6}pf∗​=7.5×106 ns52.5 ns​≈7×10−6的错误率,即每百万次访问中约七次错误,就足以使页错误带来的性能损失等于所有其他访问成本的总和。这展示了罕见、高延迟事件的非线性影响。

另一个更常见的性能障碍来自操作系统自身的活动。为了运行多个程序,操作系统会执行​​上下文切换​​,快速交换在CPU上运行的进程。出于安全考虑,这通常需要刷新TLB,清空缓存的翻译记录。因此,新进程以“冷”启动,在TLB中重建其工作集时会经历一连串的TLB未命中。这种预热代价,虽然对于单次切换来说很小,但每秒会发生数百或数千次。我们可以将其视为一种微小的性能“税”,分摊到一个进程所进行的数百万次内存引用上。这种税,虽然按每次访问计算很小,却是对性能的持续拖累,是强大的多任务处理幻象的直接后果。

同伴的复杂性:多核架构

到目前为止,我们的旅程都假设只有一个处理器。然而,现代系统是拥有多个核心的繁华都市,所有核心都可能访问同一个共享内存。这引入了两个新的、引人入胜的复杂层面:内存地理分布和社交互动。

首先是地理分布。在一个​​非统一内存访问(NUMA)​​系统中,主存被分配到多个域中,每个域对于一组核心来说是“本地”的。访问本地内存速度快(tℓt_{\ell}tℓ​),而访问另一个核心域中的“远程”内存则由于需要通过互连设备进行额外的传输而较慢(trt_{r}tr​)。现在的平均内存延迟取决于进行本地访问的概率ppp:AMAT=p⋅tℓ+(1−p)⋅tr\mathrm{AMAT} = p \cdot t_{\ell} + (1-p) \cdot t_{r}AMAT=p⋅tℓ​+(1−p)⋅tr​。

突然之间,一个软件问题出现了:操作系统应该把程序的数据放在哪里?“首次接触”(first-touch)策略会将页面放置在首次访问它的核心的本地内存中。“页面交错”(page-interleaving)策略则将页面条带化地分布在所有内存域中以分散负载。对于一个固定在单个核心上的单线程程序,“首次接触”策略是明显的赢家,它能确保所有访问都是本地的(p=1p=1p=1)。而采用交错策略,其一半的访问将是远程的(p=0.5p=0.5p=0.5)。更优策略带来的加速比是一个简单而优雅的延迟比率:tℓ+tr2tℓ\frac{t_{\ell} + t_{r}}{2t_{\ell}}2tℓ​tℓ​+tr​​。一个智能的操作系统甚至可以动态迁移页面,将数据移近最常使用它的核心,从而主动重塑系统的性能格局。

其次是社交互动。当多个核心缓存相同的数据时,我们需要规则来保持它们视图的一致性。这就是​​缓存一致性​​(cache coherence)。这些规则可能导致一个微妙但毁灭性的性能陷阱,称为​​伪共享​​(false sharing)。想象一下,两个线程在两个不同的核心上,各自更新一个独立的变量。如果这两个变量恰好位于同一个64字节的缓存块中,这两个核心就会为争夺这个缓存块而“打架”。核心0写入,获得独占所有权并使核心1的副本失效。然后核心1写入,迫使其从核心0获取该块,这反过来又使核心0的副本失效。尽管线程没有共享数据,但它们共享了缓存块,导致它在核心之间“乒乓”传送。每一次写入都变成了一次缓慢的一致性未命中,将一次完整的缓存到缓存的传输延迟加到本应是快速的L1命中上。这揭示了性能不仅取决于你访问什么数据,还取决于它在内存中以字节为单位的布局方式。

机器中的幽灵:现代延迟挑战

延迟的原理并非静止不变;它们随着我们的技术发展而演变。最后有两个“幽灵”困扰着现代系统,挑战着我们理解的边界。

第一个是​​尾延迟​​(tail latency)的幽灵。我们喜欢谈论平均性能,但在大规模的仓库级计算机中,罕见事件随时都在发生。一次内存访问可能会因为网络拥塞、系统维护的短暂暂停或十几种其他不可预测的因素而延迟。这些延迟并不遵循一个整齐的钟形曲线;它们通常有一个“重尾”,意味着超长的延迟比人们想象的更常见。一个其延迟由重尾帕累托分布描述的系统,其平均未命中代价可能远高于一个具有简单、确定性未命中时间的系统,即使它们的“典型”延迟看起来相似。这会急剧增加总体的每指令周期数(CPI),揭示了关注平均情况可能具有危险的误导性;必须为异常值进行设计。

第二个幽灵是​​虚拟化​​的成本。许多现代系统在虚拟机内部运行,其中客户机操作系统看到的“物理”内存本身就是一个由宿主机 hypervisor 管理的虚拟构造。这就创建了一个两阶段的翻译过程:从客户机虚拟地址到客户机物理地址,然后再从客户机物理地址到真正的主机物理地址。客户机中的一次TLB未命中可以触发对其页表的复杂遍历。然而,该遍历的每一步都需要自身的翻译,这又可能触发对宿主机页表的遍历。这是一个令人眼花缭乱的递归过程,可谓“层层叠叠,无穷无尽”,每一层抽象都增加了另一个潜在的延迟源。

从缓存命中的简单概率游戏到虚拟化地址翻译的递归复杂性,内存延迟的概念是一条主线,它统一了硬件架构、操作系统,乃至大规模计算的统计性质。这是一个关于巧妙欺骗、管理权衡和为每一纳秒而战的故事。

应用与跨学科联系

当我们初次思考计算机时,我们常常会想到处理器,那个进行所有思考的“大脑”。但在计算机中,“思考”的行为是处理器与其内存之间一场持续而狂热的舞蹈。处理器无法对它没有的数据进行计算,而获取数据所需的时间——即内存延迟——不仅仅是一个技术细节。它是一种决定所有现代计算节奏的基本韵律。对物理学家来说,它就像光速一样;一个你无法打破,但必须巧妙绕过的普适常数。

在我们之前的讨论中,我们剖析了这种延迟的机制,探索了缓存、转译旁观缓冲器和页表的复杂层级结构。现在,我们将开始一次更宏大的旅程。我们将看到延迟这个单一概念如何在计算的每个层面产生回响,塑造着从硅芯片设计到广阔的全球云行为的一切。这是一个绝佳的例子,说明一个单一的基本约束如何在无数个学科中催生出种类繁多、构思巧妙的解决方案。

驯服延迟猛兽:处理器设计的艺术

让我们设身处地为芯片架构师着想。你有一笔固定的硅片面积预算来构建缓存,即处理器私人的、闪电般快速的便笺簿。你面临一个经典的工程权衡。你可以设计一个简单的直接映射缓存,它检查起来非常快(命中时间短),但相当“愚蠢”——两个碰巧映射到同一位置的数据会不断地将对方踢出,即使缓存的其余部分是空的。或者,你可以构建一个更复杂的全相联缓存,它在放置数据方面要聪明得多(实现更低的未命中率),但这种搜索整个缓存的额外复杂性使其在每次访问时检查得更慢。

哪个更好?答案并非绝对。目标是最小化平均内存访问时间(AMATAMATAMAT),它同时考虑了命中的速度和未命中的高昂代价。这是一个微妙的平衡行为:如果能够显著降低访问主存那种极度缓慢情况的频率,那么稍慢一点的命中时间可能是值得付出的代价。对于许多现实世界的工作负载,最佳选择介于两者之间,或许是组相联缓存,它在两种极端之间提供了一种折中。

但是那些不可避免的缓存未命中又该如何处理?处理器速度和主存速度之间的鸿沟是如此巨大,以至于干等着是行不通的。如果你无法让访问内存的旅程更快,或许可以更早地开始这段旅程。这就是预取(prefetching)背后的哲学。如果处理器能对不久的将来需要什么数据做出有根据的猜测,它就可以提前发出请求,从而有效地隐藏延迟。

现代处理器充满了巧妙的预取逻辑。有些会寻找简单的模式,比如访问连续的内存位置(一种“步幅”)。如果一个程序正在遍历一个数组,预取器可以检测到这种模式,并在处理器甚至还未请求数组元素之前就开始获取它们。这可能非常有效,尤其是在提高转译旁观缓冲器(TLB)的性能方面,TLB负责将虚拟地址翻译为物理地址。一个好的预取器可以大幅降低有效未命中率,从而显著减少地址翻译的平均时间,并因此减少内存访问本身的时间。

我们甚至可以给予程序员或编译器直接的控制权。一些指令集允许显式的预取指令,代码本身可以告诉硬件:“嘿,过一会儿我将需要这个地址的数据。”通过计算需要多少次循环迭代才能覆盖内存延迟,编译器可以为未来几次迭代所需的数据插入一条预取指令,确保它恰好及时到达。然后,处理器核心可以不间断地继续工作,数据会在需要时神奇地出现在缓存中。这就像有一个能预知你所有需求的助手,替你走那段去图书馆的长路,让你自己永远不必去。

软硬件契约:编写尊重芯片的代码

硬件提供了这些复杂的工具,但它们并非魔法。要让它们奏效,软件必须配合。这就在程序员和芯片之间建立了一份“契约”:编写行为可预测的代码,硬件就会以惊人的速度回报你。

这一点在数据结构及其内存布局的选择上表现得尤为明显。思考一下不起眼的链表。从纯粹的算法角度来看,一个每个节点指向下一个节点的链表,无论这些节点是随机散布在内存中,还是整齐地排列在一个连续的块中,都是一样的。但从硬件的角度来看,这两种布局天差地别。

当遍历一个连续分配的链表时,每次访问都是针对一个邻近的内存地址。这对硬件来说是梦寐以求的。缓存和TLB在这种空间局部性上表现出色。在第一次访问某个内存页面后,对该页面上所有后续节点的翻译都将是速度飞快的TLB命中。相比之下,遍历一个节点随机散布的链表则是一场性能噩梦。几乎每一次节点访问都可能指向一个不同的内存页面,可能导致昂贵的TLB未命中,并迫使进行多级页表遍历。这种差异并非微不足道;对于一个典型的指针追逐任务,随机布局可能比连续布局慢近三倍,这完全是因为它违反了底层硬件为之优化的局部性原则。算法完全相同,但性能却截然不同。

现代系统的规模:“位置”至关重要

到目前为止,我们都含蓄地假设所有主存都是生而平等的。对于你的笔记本电脑或手机来说,这在很大程度上是正确的。但对于运行数据中心的强大服务器而言,这个假设不成立。这些机器通常有多个处理器插槽,每个处理器都有自己的“本地”内存库。虽然一个处理器可以访问连接到另一个处理器的内存,但它必须通过一个较慢的互连链路来完成。这种架构被称为*非统一内存访问(NUMA),它为我们的故事增加了一个新的维度:地理位置。问题不再仅仅是内存有多快*,而是它在哪里。

远程内存访问的性能损失可能非常严重,通常是本地访问延迟的两倍。如果一个程序不了解NUMA,它可能会陷入病态的性能陷阱。想象一个链表,由于采用了幼稚的分配策略,交替的节点被放置在不同的NUMA节点上。遍历这个链表的线程将被迫为每隔一个节点进行一次缓慢的远程访问。仅仅确保所有节点都分配在运行该线程的处理器的本地内存上——一种被称为“首次接触”放置的策略——就可以将遍历速度提高50%或更多。

这一原则延伸到远为复杂的软件。考虑一个由运行在不同插槽上的线程使用的高性能并发队列。如果队列的节点都分配在一个NUMA节点上,但大多数从队列中消费的线程在另一个节点上,那么每一次出队操作都将承受远程内存访问的代价。一个智能的分配策略——也许是那种特意将数据放置在最可能被使用的节点上的策略——对于可扩展性变得至关重要。这需要对算法、硬件架构和工作负载行为有深入的跨学科理解。

操作系统本身必须成为一个具备NUMA感知能力的交通警察。例如,一个抢占式调度器将一个正在运行的线程从一个处理器移动到另一个处理器的决定可能看起来无伤大雅。但如果这次移动是到一个不同的NUMA节点,那么该线程以前是本地且快速的整个工作数据集,突然之间就变得遥远而缓慢。这可能会引入巨大的、不可预测的延迟峰值。因此,现代操作系统必须努力将线程及其内存保持在同一个节点上,对允许发生损害性能的跨节点迁移的频率施加严格的预算。

即使是像Java、Go或C#这样的高级托管语言也无法逃避这一物理现实。它们的自动垃圾回收器(GC),即周期性地扫描内存以查找并回收未使用对象的机制,对NUMA效应极为敏感。如果GC工作线程必须不断地跨越缓慢的互连来扫描引用,那么一次“stop-the-world”的GC暂停(在此期间应用程序会冻结)可能会被极大地延长。具有NUMA意识的GC通过为每个NUMA节点维护独立的堆,并试图保持对象引用的局部性来应对这一问题。通过以这种方式隔离对象,它们可以显著减少缓慢的远程访问次数,从而缩短GC暂停时间、减少其破坏性,并使应用程序响应更灵敏。

抽象的层次,延迟的层次:虚拟化与云

在我们追求灵活性和安全性的过程中,我们在硬件之上构建了多层抽象。其中最强大的是虚拟化,它允许一台物理机运行多个虚拟机(VM),每个虚拟机都相信自己独占了硬件。这种魔法由 hypervisor 提供支持,它引入了另一层地址翻译:客户机的“物理”地址必须被翻译成宿主机的物理地址。

当然,这个额外的层次带来了延迟成本。在一个非虚拟化系统中只会导致一次TLB未命中的操作,现在可能会引发一连串的未命中。客户机TLB未命中会强制遍历客户机页表。但对客户机页表条目的每次访问本身就是一次内存访问,必须由宿主机的翻译机制(通常称为嵌套页表)进行翻译,而这个机制也可能会发生自己的TLB未命中!这种在每一层抽象中延迟的复合效应是虚拟化的一个基本成本,需要极其复杂的软硬件协同设计来管理。

在现代云端,特别是在无服务器计算中,所有这些线索汇集得最为生动。当你在一个无服务器平台上运行一个函数时,提供商可能会为你启动一个新的容器——这就是“冷启动”。你的函数代码及其所有依赖项,可能达数百兆字节,并未预先加载。它们是使用*按需分页*从存储中获取并映射到内存中的。每当你的函数第一次尝试访问一段代码或数据时,就会触发一次页错误,这是一个极其缓慢的事件,可能需要数毫秒。一次命中“热”实例(其中必要的页面已在内存中)的调用可能瞬间完成。但一次冷启动,背负着数千次页错误的延迟负担,可能需要几秒钟。这是内存延迟的表现形式,不是缓存中的纳秒,而是用户能明显感受到的延迟。云提供商不懈地努力以最小化这些冷启动,他们维护着热实例池,并开发出越来越聪明的方法来预测使用情况并预加载代码,所有这些都是为了对抗延迟。

最后的话:可预测性与无情的时钟

我们已经看到计算世界如何持续地与平均情况下的内存延迟作斗争。但在某些领域,平均值无关紧要;最坏情况才是一切。在一个硬实时系统中,比如飞机的飞行控制器或医疗设备的监控系统,错过最后期限是不可接受的。

在这些系统中,像按需分页这类技术的不可预测性是不可接受的。一次单一的、意外的页错误就可能导致错过最后期限,并带来灾难性后果。对于这些应用,设计者必须在严格的延迟预算下运作。他们必须计算出在保证作业能满足其最后期限的前提下,所允许的最大页错误概率,然后设计系统以确保永远不会超过该界限。有时,这意味着禁用性能增强功能或将任务的整个工作集预加载到内存中,以确保其执行时间是确定且有界的。

从缓存命中的纳秒时间尺度,到云函数冷启动的长达数秒的延迟;从单个芯片内的权衡,到全球数据中心的架构;从程序员对数据结构的选择,到操作系统调度器的策略——原理都是相同的。宇宙为我们移动信息的速度设定了一个极限。计算的故事,就是我们为了驾驭那个简单、优美而又无情的约束所做的、不懈而又充满奇妙创造力的努力的故事。