try ai
科普
编辑
分享
反馈
  • 多级缓存

多级缓存

SciencePedia玻尔百科
核心要点
  • 多级缓存通过利用引用局部性原理,弥合了高速处理器与慢速主存之间的“内存墙”。
  • 平均内存访问时间(AMAT)是用于建模和优化缓存层次结构性能的基本公式,它通过平衡命中时间和未命中率来实现优化。
  • 缓存设计涉及关键的权衡,例如通过组相联来减少冲突未命中,以及通过包含/独占策略来管理多核一致性。
  • 缓存架构的选择并非孤立的硬件决策;它们直接影响操作系统的调度效率、算法的复杂性以及系统面对侧信道攻击的脆弱性。
  • 现代缓存设计不仅要平衡速度(AMAT),还要考虑功耗和物理面积的限制,这导致了诸如能量延迟积(EDP)等复杂的优化。

引言

在现代计算中,处理器闪电般的速度与主存相对缓慢的步调之间存在巨大的性能鸿沟。这个被称为“内存墙”的鸿沟,有可能让处理器因等待数据而处于空闲状态。解决方案是多级缓存,一个由更小、更快的存储(L1、L2、L3)组成的复杂层次结构,它充当了中介,基于引用局部性原理来预测数据需求。本文将探讨多级缓存的复杂世界,阐述其基本设计挑战及其深远影响。

本次探索分为两个关键部分。首先,在“原理与机制”部分,我们将剖析缓存的核心机制。您将学习如何使用关键的平均内存访问时间(AMAT)指标来衡量其性能,并探讨架构师面临的关键设计权衡,包括缓存组织结构、包含与独占等缓存间策略以及写策略。随后,“应用与跨学科关联”部分将揭示这些底层硬件决策如何在整个系统中产生连锁反应,影响操作系统的功能,重新定义算法的效率,并催生出计算机安全领域的新型漏洞。

原理与机制

在我们探索计算机制的征途中,我们经常会遇到一个深刻而反复出现的主题:与瓶颈的斗争。现代计算机处理器拥有惊人的速度,能够在眨眼之间执行数十亿条指令。然而,这位短跑健将却被一个相对笨重的伙伴——主存储器(RAM)——所束缚。如果处理器每次需要数据都必须等待主存,那么它将大部分时间都处于空闲状态,就像一位世界级厨师被迫从遥远的仓库中取回每一种食材。处理器速度与内存速度之间的这种鸿沟,就是著名的​​内存墙​​。

解决这一困境的方法不是让仓库变得更快——这在成本和物理上都极具挑战性——而是在厨房旁边建一个小型、极快的储藏室。这个储藏室就是​​缓存​​。通过预测厨师很快会需要哪些食材并提前获取它们,我们可以让厨师全速工作。这种预测的原理根植于一个关于计算机程序的优美而简单的观察,即​​引用局部性​​:

  • ​​时间局部性:​​ 如果一个数据被使用,它很可能在不久后再次被使用。
  • ​​空间局部性:​​ 如果一个数据被使用,其附近内存地址的数据也很可能在不久后被使用。

单个缓存已经很好,但局部性的逻辑不止于此。如果一个小储藏室很有用,为什么不在附近再建一个稍大、稍慢的储藏间,并由主仓库为其供货呢?这就是​​多级缓存​​层次结构的诞生,它是一系列级联的存储——通常标记为​​L1​​、​​L2​​和​​L3​​——每一级都比前一级更大、更慢、更便宜,从而在内存墙上架起一座复杂的桥梁。

万物的标尺:平均内存访问时间

为了驾驭设计这一层次结构时的复杂权衡,我们需要一个指南针。这个指南针就是​​平均内存访问时间(AMAT)​​。它回答了一个简单的问题:“平均而言,一次内存请求需要多长时间?”AMAT的美妙之处在于其优雅的递归结构,完美地反映了缓存层次结构本身。

对于一个三级缓存,AMAT可以用一个极其直观的公式来表示:

AMAT=t1+m1×(t2+m2×(t3+m3×tmem))\mathrm{AMAT} = t_1 + m_1 \times (t_2 + m_2 \times (t_3 + m_3 \times t_{\text{mem}}))AMAT=t1​+m1​×(t2​+m2​×(t3​+m3​×tmem​))

让我们来分解这个公式。每次内存访问都从最快的L1级开始。在那里找到数据所需的时间是​​L1命中时间​​,t1t_1t1​。但有时数据并不在那里——这种情况称为​​L1未命中​​。这以一定的概率发生,即​​L1未命中率​​,m1m_1m1​。当发生未命中时,我们就要付出代价:必须去下一级,即L2中搜索。

L1未命中的代价是第一个括号中的整个表达式:(t2+m2×(...))(t_2 + m_2 \times (...))(t2​+m2​×(...))。它等于访问L2的时间(t2t_2t2​)加上同样在L2中未命中的代价,这种情况发生的概率是m2m_2m2​。这种嵌套逻辑一直延续下去,直到我们遇到最终的代价:访问慢速主存,这需要时间tmemt_{\text{mem}}tmem​。

这个简单的方程是缓存设计师的“北极星”。他们的目标是通过调整每个变量来最小化AMAT。他们可以通过使用更快的电路来缩短命中时间(tit_iti​),或者通过使缓存更大或更智能来降低未命中率(mim_imi​)。例如,设计师可能面临为L1缓存选择最佳大小和配置的任务,同时L2和L3是固定的。通过为特定工作负载建模,分析不同的L1容量(S1S_1S1​)和块大小(BBB)如何影响未命中率,他们可以将这些值代入AMAT公式,找到产生最低可能延迟的配置。

但“时间”究竟是什么?我们公式中的命中时间和未命中惩罚时间并非单一的数值。一次内存访问是硬件组件之间一场错综复杂的协同运作。检测到L1未命中、为通往L2的共享总线进行仲裁、查找L2自己的目录(其标签),以及最终将数据传回,这是一个多阶段的流水线。巧妙的工程设计允许这些阶段重叠。例如,系统可能在最终确定总线请求的同时,就开始查找L2的标签目录。通过这种​​流水线重叠​​的方式这里省一个周期、那里省一个周期,可以显著降低有效的未命中惩罚,从而直接降低AMAT并提升实际性能。

将缓存视为图书馆:组织结构与冲突

缓存如何决定数据存储在哪里?想象一个图书馆。如果你可以把任何一本书放在任何地方,那么要找到它就需要搜遍整栋建筑。这就是​​全相联​​缓存——极其灵活,但对于除极小型缓存外的所有缓存来说,其速度慢得令人无法接受,成本也高得惊人。

在另一个极端,想象每本书在某个特定书架上都有且仅有一个指定的位置。这就是​​直接映射​​缓存。检查一本书是否存在非常快——你只需要看一个地方。但如果两本经常使用的书被分配到同一个位置会发生什么?它们会不断地将对方驱逐出去,导致一种称为​​冲突未命中​​的现象。缓存的其他地方有很多空位,但却无法使用。

介于两者之间的折中方案是​​组相联​​缓存。在这里,一本书被分配到一个特定的书架(一个​​组​​),但它可以被放置在该书架上少数几个位置(AAA,即​​相联度​​)中的任意一个。这种设计在查找速度和缓解冲突的灵活性之间取得了平衡。

然而,即使是组相联缓存也容易受到病态访问模式的影响。考虑一个程序,它以固定的步长遍历一个大表。如果这个步长与缓存的几何结构——特别是组的数量——“合谋”,就可能导致多个访问反复映射到同一个组。如果冲突访问的数量超过了该组的相联度,缓存就会发生颠簸,刚刚驱逐的数据很快又需要被再次加载。在这种最坏的情况下,未命中率可能从接近零飙升到100%,将我们的高速储藏室变成一扇旋转门。

为了在不付出更高相联度代价的情况下应对这些有害的冲突未命中,架构师们设计了一个优雅的附加组件:​​受害者缓存​​。这是一个小型的、全相联的缓存,位于L1旁边。当一个缓存行从L1中被驱逐——成为“受害者”——它会被放入受害者缓存,而不是被丢弃。如果处理器在短时间内再次请求该行(这在冲突场景中很常见),它就能在受害者缓存中快速命中,从而避免了到L2的漫长旅程。通过对一个缓存行的重用概率进行建模,我们可以精确地量化一个小小的受害者缓存能在多大程度上降低整体未命中率,这通常能以很小的硬件投资带来显著的性能提升。

两种策略的故事:包含与独占

有了多级缓存,一个基本的哲学问题随之产生:如果一个数据块在L1中,它的副本是否也应该存在于L2中?对这个问题的两个答案定义了主要的缓存间策略。

  • ​​独占策略(Exclusive Policy):​​ L1和L2缓存是​​不相交的​​。任何数据都不会同时存在于这两个级别中。当一个缓存行被调入L1时,它会从L2中移除。主要优点是最大化了芯片上存储的唯一数据块的总数。总的有效缓存大小就是各个容量之和,CL1+CL2C_{L1} + C_{L2}CL1​+CL2​。

  • ​​包含策略(Inclusive Policy):​​ L2缓存是L1缓存的​​超集​​。任何在L1中找到的数据,都保证在L2中也有一个副本。这看起来很浪费,因为总的唯一数据受限于L2的容量CL2C_{L2}CL2​,并且L2的很大一部分空间被用来复制L1的内容。

那么,为什么会有人选择包含策略呢?答案在于多核处理器的世界。在一个拥有多个核心、每个核心都有自己私有L1缓存的系统中,我们必须确保​​一致性​​——即所有核心对内存都有一个一致的视图。如果一个核心写入某个内存位置,其他持有该数据副本的核心必须得到通知。

一个包含性的共享L2缓存在这里提供了巨大的优势。因为L2包含了所有L1缓存中所有内容的副本,所以它可以充当一个​​窥探过滤器​​。当来自某个核心(或外部设备)的内存请求到达时,系统只需检查L2的标签。L2目录确切地知道哪个(或哪些)其他L1缓存持有该行。然后,它只需向需要它的核心发送一个定向的无效化消息,而无需向系统中的每个核心广播破坏性的窥探请求。这种过滤极大地减少了一致性流量并提高了系统性能。其代价是浪费了重复的空间,并且需要​​反向无效化​​:当一个缓存行从L2被驱逐时,必须向L1发回消息以将其也从L1中驱逐,从而维护包含属性。计算这样一个系统中的AMAT,需要将这种过滤后的一致性流量所带来的预期延迟加到未命中惩罚上,从而得到一幅完整的性能图景。

看不见的劳动:写入内存

我们的讨论一直集中在从内存中读取数据,但写入数据同样重要。在这里,架构师也面临一个关键的策略选择。

对于​​写通​​(write-through)策略,每当处理器向缓存写入时,数据也会立即被写入内存层次结构的下一级。这种方式简单,并能确保下层总是一致的。然而,它会产生大量的流量。

另一种选择是​​写回​​(write-back)策略。当处理器向缓存写入时,数据只在该缓存中被修改,并且该行被标记为​​脏​​(dirty)。新的数据只有在该脏行最终被驱逐时,才会被写回到下一级。如果一个程序多次写入同一行,这种方式效率要高得多,因为它将多次写入合并为一次驱逐时的写回。然而,它增加了系统的复杂性。此外,这些写回操作会消耗连接缓存与内存的总线带宽。通过对脏行驱逐率进行建模,我们可以计算出由此产生的总线利用率,这是影响整体系统平衡和性能的一个关键因素。

现代综合:平衡速度、功耗与成本

一次内存请求的旅程,从AMAT方程到一致性的复杂细节,揭示了缓存设计是一项宏伟的平衡艺术。在早期,唯一的目标是最小化延迟。如今,这个问题是多维度的。

  • ​​能量:​​ 缓存不仅消耗时间,还消耗能量。例如,提高缓存的相联度可以减少冲突未命中,但每次访问都需要更复杂、更耗能的电路。现代设计师不仅为AMAT优化,他们还为​​能量延迟积(EDP)​​进行优化,该指标体现了性能与功耗之间的权衡。在严格的延迟预算下,找到最小化EDP的相联度是设计高能效处理器的核心任务。

  • ​​成本与物理现实:​​ 缓存不是抽象的实体;它们是蚀刻在硅片上的物理结构,占据着宝贵的芯片面积。架构师总是受到​​面积预算​​的限制。这迫使他们在技术上做出艰难的选择。一个巨大的L3缓存应该使用传统的​​SRAM​​(静态随机存取存储器)来构建,它速度快但密度不高;还是使用​​EDRAM​​(嵌入式动态随机存取存储器),它密度高得多,可以在相同面积内容纳更大的容量,但代价是延迟更高?回答这个问题需要将物理和技术现实代入我们可信赖的AMAT公式,看看哪种方案能在不超出面积预算的情况下达到性能目标。

因此,多级缓存是现代计算中无名的英雄。它是分层设计的杰作,是局部性原理的物理体现,也是工程妥协艺术的证明。它优雅地解决了一个根本性的性能问题,同时在速度、功耗、尺寸和成本之间纷繁复杂的权衡中游刃有余。

应用与跨学科关联

在我们迄今的探索中,我们已经剖析了多级缓存的原理——这个位于处理器飞速运转的大脑和浩瀚的主存库之间的巧妙内存层次结构。我们已将其视为工程杰作,以追求速度为唯一目标而设计。但如果止步于此,就好比研究了小提琴的解剖结构却从未听过它演奏的音乐。缓存层次结构的真正美妙之处不在于其孤立的存在,而在于它与整个计算世界产生的深刻且常常出人意料的互动。它不仅仅是一个组件;它是一个活跃的舞台,操作系统、算法乃至计算机安全的戏剧都在其上上演。

作为内存编排者的操作系统

也许最基本的伙伴关系是缓存层次结构与操作系统(OS)之间的关系。操作系统为我们提供了强大的抽象,使编程变得理智和易于管理,但这些抽象往往伴随着高昂的性能成本。正是缓存使它们不仅成为可能,而且变得实用。

以虚拟内存为例,这是操作系统营造的宏大幻象,即为每个程序提供其独有的、连续的地址空间。为了维持这个幻象,处理器必须将程序中的“虚拟”地址转换为内存中的“物理”地址。这个转换是通过遍历一个名为页表的数据结构来完成的。在一个深度为ddd的多级页表系统中,单次转换可能需要ddd次对主存的独立访问。若没有任何缓存,其时间成本将是一个 crushing 的惩罚,与ddd倍的内存延迟LLL成正比。这种根本性的矛盾会使我们现代的多任务计算机变得极其缓慢。

当然,构成这张地图的页表项(PTE)也只是数据,而数据是可以被缓存的。魔法就此开始。一个专门的缓存,即转译后备缓冲器(TLB),保存了最近使用的翻译结果。当TLB未命中时,硬件开始其页表遍历,但现在它不必每一步都一直走到主存。它会先检查L1缓存,然后是L2,再是末级缓存(LLC)。一次内存访问的平均时间变成了一场精妙的概率之舞,它综合考虑了TLB查找的时间、TLB未命中的概率,以及在缓存层次结构各级找到PTE的加权平均成本。多亏了缓存,地址转换那惊人的理论成本被降低到一个微小、可控的开销。缓存层次结构使得虚拟内存这一优美的抽象得以成功实现。

这种协作甚至可以更加智能。许多程序并非随机访问内存,而是以可预测的模式进行,比如遍历一个大数组。一种名为“预取器”的巧妙硬件机制可以检测到这种模式。当看到对页面vvv的请求后,它可能会猜测程序很快会需要页面v+Sv+Sv+S,其中SSS是观察到的步长。然后,它可以在这些未来的访问被请求之前,就主动将它们的页表项取入缓存。这使得系统从纯粹的被动反应转变为预测性行动,将一次可能漫长的页表遍历等待变成一次瞬时的缓存命中。

缓存的影响还延伸到操作系统如何调度程序。在多核处理器中,操作系统可能会决定将一个正在运行的线程从一个核心迁移到另一个核心以平衡负载。这一看似简单的行为会产生深远的性能影响,而这些影响直接取决于缓存策略。如果核心共享一个包含性的LLC——即LLC持有私有L1/L2缓存中所有内容的超集——那么迁移的线程仍有机会在共享缓存中找到其“温热”的工作数据。LLC充当了安全网。相反,如果层次结构是独占性的——即LLC仅持有不在私有缓存中的数据——那么线程的数据是其旧核心的本地数据。迁移后,它会发现新核心的缓存是冷的,并且在LLC中也找不到数据,从而导致一场昂贵的未命中风暴。包含性与独占性缓存的选择是一个深层的微架构决策,它直接影响操作系统调度策略的成本,迫使操作系统成为一个能感知缓存的线程编排者。

非均匀世界中的算法

计算机程序的世界并非教科书所暗示的光滑、均匀的空间。它是一个“非均匀”的世界,有几个小而极快的位置(缓存)和一个巨大而缓慢的位置(主存)。一个算法的性能往往不是由它执行了多少次计算决定的,而是由它在这个非均匀地形中导航的能力决定的。

想象一个计算求解器,通过计算其算术步骤,其运行时间本应与问题规模的平方成正比,即Θ(N2)\Theta(N^2)Θ(N2)。然而,当我们测量其性能时,却发现其运行时间更像是O(N1.8)O(N^{1.8})O(N1.8)。这怎么可能呢?答案是,该程序并非受限于其计算速度,而是受限于它从主存移动数据的速度——它是内存带宽受限的。这种次二次方扩展的原因是​​缓存分块​​的胜利。通过重构算法,将一小块数据加载到缓存中,并在其被驱逐前对其执行所有可能的操作,我们极大地减少了到主存的总流量。O(N1.8)O(N^{1.8})O(N1.8)的扩展是一个标志,表明随着问题规模的增长,该算法在数据重用方面变得越来越高效,这是优秀的缓存感知设计的标志。

这一原则迫使我们重新思考“复杂性”的真正含义。Strassen矩阵乘法算法著名的Θ(nlog⁡27)\Theta(n^{\log_2 7})Θ(nlog2​7)复杂度是一个理想化的结果,它假设所有内存访问都是等价的。一个更完整的模型揭示了真实的运行时间是算术时间和通信时间之和——即在缓存级别之间移动数据所花费的时间。这个通信成本是一个复杂的函数,它取决于层次结构中每一级的缓存大小MiM_iMi​和缓存行大小BiB_iBi​。要做到真正的快速,算法的设计不仅要最小化算术操作,还要在分层世界中最小化数据移动。

这种理念一直延伸到数据结构的选择。考虑在外部排序中合并许多已排序列表的任务,这个过程通常由一个最小堆来管理。一个经典的二叉堆,在理论上如此优雅,在实践中却表现不佳。作为堆核心功能的“下沉”操作,涉及到从父节点跳转到子节点。在基于数组的二叉堆实现中,这些节点在内存中可能相距很远,导致空间局部性差和一连串的缓存未命中。解决方案是设计一个考虑缓存的数据结构。通过使用ddd叉堆(每个节点有d>2d>2d>2个子节点),结构变得更短更宽。现在,一个节点的所有ddd个子节点都连续存储在内存中。找到最小的子节点需要多做一些比较,但这通常可以通过一次缓存行填充来完成。这是一个绝妙的权衡:我们接受多一点计算工作,来换取内存延迟的大幅降低——这在任何现代处理器上都是一个稳赢的赌注。

机器中的幽灵:当缓存背叛我们时

缓存被设计成隐形的工作马,默默地为我们的计算加速。但它们的操作本身,它们对机器状态产生的物理影响,可能会泄露秘密。就像雪地里的脚印一样,缓存在状态上的变化可以揭示秘密操作的踪迹。这就是侧信道攻击的基础。

一个简单而强大的攻击是“填充+探测”(Prime+Probe)。攻击者用自己的数据填充共享缓存的一部分(“填充”)。一段时间后,他们再次访问这些数据(“探测”)。如果受害者进程访问了映射到相同缓存位置的内存,它就会驱逐掉一些攻击者的数据。攻击者会在探测阶段注意到访问时间变长。

在这里,一个看似无害的设计选择——包含性缓存策略——可能成为一个安全隐患。在包含性层次结构中,从共享LLC中驱逐一个缓存行会强制使持有该行的任何私有L1或L2缓存中的相同行也失效。这会产生一个“驱逐级联”。攻击者在LLC中的行为产生了放大的效应,相比于缓存之间更为隔离的非包含性系统,它创造了一个更响亮、更容易被检测到的信号。

当与现代CPU中最强大的性能优化之一——推测执行——相结合时,这种脆弱性变得更加强大。为了保持其流水线充满,CPU不断对程序的未来走向做出预测,并基于这些猜测“瞬态地”执行指令。如果猜测错误,结果会被丢弃,但微架构层面的副作用——缓存在状态上的“脚印”——依然存在。这就是使Spectre等攻击成为可能的机器中的“幽灵”。

缓存策略再次决定了这个幽灵的可见程度。假设一条瞬态指令需要从内存加载数据。在一个采用包含性LLC的系统中,必须遵守包含规则:要将该行带入L1,也必须将其带入LLC。这为“填充+探测”攻击者留下了明确、可观察的痕迹。然而,在一个采用独占性LLC的系统中,该行可能直接从内存进入L1,完全绕过LLC。在这种情况下,幽灵的经过在共享缓存中没有留下任何痕迹供攻击者查看。在这种背景下,独占策略削弱了侧信道信号,使系统更加健壮。缓存策略的选择不仅仅是一个性能决策;它还是一个关键的安全权衡。

从使我们的操作系统成为可能,到重新定义算法效率的真正含义,再到开辟计算机安全的新前沿,多级缓存是计算故事中的核心角色。它是一个充满优美复杂性的地方,是深邃且常常出人意料的统一性的证明,这种统一性将现代计算机的每一层紧密联系在一起。