try ai
科普
编辑
分享
反馈
  • 包容性缓存

包容性缓存

SciencePedia玻尔百科
核心要点
  • 包容性缓存通过确保末级缓存(LLC)包含所有上层缓存数据的超集,充当单一目录,从而简化了多核一致性。
  • 包容性策略的主要缺点是反向无效机制,即LLC中的驱逐会强制从私有缓存中移除数据,从而导致核心之间的性能干扰。
  • 包容性属性会产生可观察的副作用,这些副作用可被用于安全攻击,例如 Flush+Reload 攻击,它可以在处理器核心之间泄露信息。
  • 包容性缓存降低了缓存层次结构的总唯一存储容量,因为上层缓存仅持有LLC中已存在数据的副本。
  • 这一设计选择具有深远的影响,从同步性能和硬件预取到虚拟机迁移和算法设计,无所不包。

引言

在计算机复杂的存储系统层次结构中,数据在庞大但缓慢的主存与紧邻CPU的微小但极速的缓存之间流动。整个系统的效率取决于一套管理这些不同层级数据存储与访问的规则。其中最基本的一条规则便是包容性策略,这一设计选择对性能、安全乃至整个系统的行为都产生了深远的影响。本文旨在解决一个关键问题:尽管包容性策略存在诸如有效存储容量减少等明显缺点,为何设计者仍会选择它。

本次探索将引导您进入包容性缓存的复杂世界。第一章​​“原理与机制”​​将剖析包容性的核心概念,将其与排他性策略进行对比,并解释在简化的数据一致性与反向无效及资源争用带来的性能陷阱之间的关键权衡。随后,​​“应用与跨学科联系”​​一章将拓宽视野,揭示这一单一架构决策如何产生涟漪效应,影响多核软件同步、制造重大的网络安全漏洞,甚至影响数学算法和大规模云系统的设计。

原理与机制

想象一下,计算机的存储系统是一个庞大而层级分明的图书馆。在最远处, sprawling and slow,是国家级的主档案馆——​​主存​​。离研究员——​​CPU核心​​——更近的地方,有一些更小、更快、更方便的分支图书馆。这些就是​​缓存​​,通常按层级排列:一个微小、极速的一级(L1)缓存,一个较大、稍慢的二级(L2)缓存,或许还有一个更大的三级(L3)缓存,作为前往主存漫长旅途前的最后一站。这个系统的根本挑战在于,如何将最常用的书籍(数据)存放在最近、最快的图书馆里。但这些图书馆应如何协调呢?一个简单而深刻的策略决策在此应运而生:包容性规则。

图书管理员的指令:包容性 vs. 排他性

假设我们的图书馆系统采纳了一条严格的规则:一个地方分支图书馆(如L1缓存)只有在同一本书的副本同时被主区域图书馆(如下级的共享缓存,如L3)持有时,才能持有这本书。这就是​​包容性缓存​​层次结构的精髓。L1缓存中的所有数据集合是L2缓存中数据的*子集*,而L2缓存中的数据又是L3缓存中数据的子集。

另一种选择是​​排他性缓存​​策略。在这里,规则旨在最大化存储空间:如果一个地方分支借出了一本书,区域图书馆就会从自己的书架上移除这本书,以便为另一本独一无二的书腾出空间。各级缓存的数据集是互不相交的。

这一个决策对系统的总有效存储容量产生了巨大而直接的影响。在一个包容性层次结构中,由于上层缓存仅仅是下层缓存内容的副本,整个缓存系统能容纳的唯一数据块总数就是最大一级、即末级缓存(LLC)的容量。如果L3缓存是101010 MiB,那么整个系统就只能容纳101010 MiB的唯一数据,无论L1和L2缓存有多大。

而在一个排他性层次结构中,容量是累加的。一个111 MiB的L1和一个101010 MiB的L2,其作用就像一个111111 MiB的统一缓存。这种差异并不仅仅是学术上的;它具有深远的性能影响。考虑一个程序,其工作集——即需要经常访问的数据总量——为121212 MiB。在一个拥有256256256 KiB L1和444 MiB L2(总有效容量4.254.254.25 MiB)的排他性系统中,这个程序会发生抖动,不断地访问主存。但如果L2的容量有121212 MiB,它就能完美容纳。而在一个拥有相同L1和一个121212 MiB L2的包容性系统中,如果工作集是131313 MiB,它就会持续抖动。L1的容量没有为工作集提供额外的存储空间,它只是为L2已持有数据的一个子集提供了更快的访问速度。这种抖动的代价是巨大的,可能会将执行速度减慢几个数量级。

既然包容性缓存似乎“浪费”了其上层缓存的容量,为什么会有设计师选择这种策略呢?答案,正如在工程领域中常见的那样,是用一个问题换取了对一个更棘手问题的巧妙解决方案。

秩序之美:简化一致性

现代计算的世界是一个多核的世界。我们简单的图书馆系统现在有了多个独立的研究员(核心),每个都有自己的私人分支图书馆(私有L1/L2缓存)。然而,他们都共享对大型区域图书馆(共享末级缓存,LLC)的访问权限。现在,一个关键问题出现了:如果一个核心决定在一本书的页边空白处“做笔记”(修改一个数据块),它必须通知所有其他拥有这本书副本的核心,告知他们的版本现已过时。这就是​​缓存一致性​​问题,一个后勤上的噩梦。

这正是包容性策略的优雅之处。因为共享的LLC必须包含存在于任何私有缓存中的数据块的副本,它就成为了整个系统的单一事实来源。要查明哪些核心拥有一个数据块的副本,一个核心无需向其他所有核心“喊话”;它只需检查LLC的目录即可。LLC的目录保证拥有所有共享者的完整记录。这极大地简化了在芯片上保持数据一致性所需的硬件和协议,这在复杂的多核设计中是一个巨大的优势。

在一个非包容性或排他性的世界里,找到所有副本要困难得多。数据是在核心0的L1里?还是核心1的L1里?在LLC里?还是三者都有?逻辑变得错综复杂。包容性策略施加了一种简单的层次化秩序,而这种秩序带来了简单性和正确性。

秩序的代价:反向无效风暴

然而,这种优雅的秩序是通过一个有其阴暗面的机制来维持的。当共享的LLC已满,需要驱逐一个块以便为新块腾出空间时,会发生什么?为了维护其指令,LLC必须确保被驱逐块的任何副本都不会留在任何上层缓存中。它通过向上传递一个命令——​​反向无效​​——来实现这一点。这就像区域图书馆宣布:“我正在丢弃馆藏的《战争与和平》。所有分馆必须立即丢弃各自的副本。”

这意味着缓存中较低、共享层级的活动可以强制销毁较高、私有层级的数据。这可能导致灾难性的性能场景。想象一个双核记。核心1正在处理一个小的、重要的数据集,这个数据集完美地装在它的私有L2缓存中。它运行得快速而高效。与此同时,核心0出于完全无关的原因,开始了一个需要大量新数据的大规模流式操作。这个数据流淹没了共享的L3缓存。随着L3被填满,它开始驱逐数据块以腾出空间。不幸的是,它驱逐的某些块恰好是核心1正在积极使用的那些。

每当L3驱逐一个属于核心1的块时,它就会向核心1的L2发送一个反向无效指令,销毁其副本。当核心1恢复工作时,它发现自己曾经的热数据已从私有缓存中消失,迫使其承受一场缓慢的未命中风暴。核心1的性能被一个完全独立的核的行为所破坏,这种干扰只有在包容性策略下才可能发生。一个非包容性系统则不受影响;L3的驱逐不会对核心1的私有L2产生任何作用。

包容性不平衡:异构世界中的不公平

在当今常见的异构处理器中,这种干扰变得更加微妙和有害,这些处理器混合了强大的“大”核和高效的“小”核。想象一个大核拥有一个大的64 KiB L1缓存,一个小核拥有一个小的32 KiB L1缓存,它们都共享一个包容性的L3缓存。

由于包容性规则,大核的大L1实际上“预留”了共享L3中64 KiB的空间,用于存放其私有数据的副本。而小核只预留了32 KiB。大核凭借其规模,在共享资源中占据了更大的份额。这就像一位富有的赞助人要求在公共图书馆中划出一大片“保留”区域,只为存放他私人书房中书籍的副本,从而给普通公众留下了更少的空间。

这减少了小核可用的有效L3容量。小核的L1更小,因此更依赖于共享的L3,却发现自己的“游乐场”变小了。这可能导致一个悖论性的结果:即使运行的工作负载特性相同,小核的L3未命中率也可能高于大核。在这种情况下,包容性策略造成了一种固有的不公平,强者愈强,弱者愈弱。

我们能预测风暴吗?

这给芯片设计者提出了一个问题:这些破坏性的干扰模式可以避免吗?我们能设计一个足够大的缓存来防止它们吗?答案是肯定的,通过定量建模。我们可以将一个程序的访问模式看作是一小组我们希望保留在缓存中的“热”行与一大股只是路过的“冷”行的混合体。

冷数据流给共享缓存带来了压力。在缓存的任何给定组(set)中,有AL2A_{L2}AL2​个可用槽位(其相联度)。如果我们需要在该组中保护HHH个热行,那么就只剩下AL2−HA_{L2} - HAL2​−H个槽位用于流式数据。总共VVV行的流式数据分布在缓存的SL2S_{L2}SL2​个组中。为保证没有热行被驱逐,映射到任何一个组的流式行数(平均为VSL2\frac{V}{S_{L2}}SL2​V​)不能超过可用的槽位数。这就导出了一个优雅的不等式:

VSL2≤AL2−H\frac{V}{S_{L2}} \le A_{L2} - HSL2​V​≤AL2​−H

通过重新排列这个不等式,设计者可以计算出保护给定工作负载所需的最小缓存容量(CL2,min⁡=SL2,min⁡×AL2×BC_{L2, \min} = S_{L2, \min} \times A_{L2} \times BCL2,min​=SL2,min​×AL2​×B)。这表明计算机体系结构并非猜测;它是一门通过严谨的数学权衡来管理有限资源的科学。

我们如何得知?窥探层次结构

所有这些策略和机制——包容性、排他性、反向无效——都在芯片深处无形地运作。一个工程师,或一个好奇的学生,如何能弄清楚某个给定的芯片正在使用哪种策略呢?答案在于设计巧妙的实验,并使用特殊的​​硬件计数器​​。

想象一下我们可以安装两个这样的计数器:

  1. ​​重复计数器​​:该计数器测量L1缓存中同时存在于L2缓存中的数据比例。对于一个严格的包容性层次结构,我们期望这个值,我们称之为ddd,非常接近1.01.01.0。对于一个严格的排他性层次结构,它应该接近0.00.00.0。

  2. ​​反向无效计数器​​:该计数器专门计算L2驱逐触发L1无效的次数。我们称其比率为III。

现在我们运行一个程序并观察结果。如果我们看到d≈0.93d \approx 0.93d≈0.93和I≈0.47I \approx 0.47I≈0.47,我们能得出什么结论?高重复率排除了排他性缓存的可能性。但关键的证据是反向无效率。一个非包容、非排他(NINE)的缓存没有理由执行反向无效;那种机制只为强制实现包容性而存在。计数器在计数这一事实本身——即近一半的L2驱逐都导致了L1无效——就是“确凿证据”。它无可辩驳地证明,该层次结构必须是包容性的。这些抽象的策略具有真实、可测量且独特的物理足迹。

因此,包容性原则是一把双刃剑。它为混乱的多核数据共享世界带来了优美而简化的秩序。但这种秩序是通过反向无效这只铁腕来维持的,这个机制可能造成微妙的不公平和灾难性的干扰。使用它的决定是一项深刻的工程权衡,是计算机体系结构核心的精妙平衡艺术的完美典范。

应用与跨学科联系

在物理学世界中,我们常常发现一个单一、优雅的原理——比如最小作用量原理——可以发展成为对宇宙广阔而复杂的描述。这是科学的一大美妙之处。在计算机体系结构领域,我们发现了类似的现象。一个看似简单的设计选择,一个施加于处理器有序混乱之上的单一规则,其后果可以向外扩散,触及从多核协同工作的交响乐到网络安全的阴影世界,甚至抽象的数学算法设计等方方面面。

缓存包容性原则,即规定末级缓存必须持有其上层更小、私有缓存中所有内容的副本,正是这样一条规则。我们已经了解了这一原理的机制;现在,让我们踏上一段旅程,去发现它为何重要。我们将看到,这一个理念如何将末级缓存从一个单纯的数据仓库,转变为一个积极而强大——尽管有时会带来问题——的整个芯片的监督者。

核心的交响乐:一致性的乐队指挥

想象一个多核处理器是一支管弦乐队,每个核心都是一位根据乐谱(数据)演奏的音乐家。如果一位音乐家决定改变一个音符,他如何确保其他所有共享这部分乐谱的人都看到这个变化?没有指挥,这位音乐家可能不得不向整个乐队大喊,造成一片嘈杂的信息。这类似于一种早期的缓存一致性方法,即需要写入数据的核心会向其他所有核心广播一条“无效”消息。

一个包容性的末级缓存(LLC)扮演着乐队指挥的角色。因为它持有一个记录了所有存在于私有缓存中数据的目录,所以它确切地知道哪些其他“音乐家”(核心)拥有一份特定“乐谱”(缓存行)的副本。当一个核心需要修改数据时,请求会发送到LLC。LLC作为指挥,不会向所有人大喊。相反,它只向持有副本的核心发送精确、定向的消息。这种从广播到定向消息的转变,极大地减少了一致性流量的背景噪音,使得核心能够更有效地通信,整个系统性能也更好。

但指挥的工作伴随着庄严的责任。为了保持其目录的准确性,LLC必须以严格的纪律执行其包容性规则。如果LLC需要腾出空间并决定驱逐一个缓存行,它不能简单地将其丢弃。它必须首先向任何持有该行的私有缓存发送“反向无效”消息,强制它们也驱逐自己的副本。如果它不这样做,它的目录就会变成谎言,一致性的保证将被打破,交响乐将陷入混乱。这种反向无效的必要性是包容性设计的根本代价;窥探过滤的优雅简洁是以这种僵化、自上而下的控制为代价的。

同步之舞:危险的瓶颈

软件的性能,尤其是同时在多个核心上运行的软件,依赖于错综复杂的同步编排。例如,自旋锁是“话语权杖”的数字版本;只有持有锁的线程才被允许进入代码的关键部分。一个朴素但常见的实现方式是使用Test-and-Set指令,等待的线程会反复尝试写入锁变量以获取它。

每一次尝试都是一次写操作,在多核系统中,每次写操作都要求一个核心获得包含锁的缓存行的独占所有权。这会引发一场“无效风暴”,即缓存行在一个个自旋的核心之间激烈地传递,每一次传递都会使前一个所有者的副本无效。包容性LLC并不能阻止这场风暴,但它深刻地改变了风暴的性质。因为LLC是所有一致性流量的中心枢纽,这场风暴被汇集到它这里。锁缓存行持续、高速的传输现在消耗了LLC的带宽和资源,有可能将这个中心指挥官变成一个瓶颈。

更糟糕的是,包容性策略造成了一个微妙的干扰漏洞。想象一下,我们等待的线程正在自旋,希望能获得锁。现在,一个完全不相关的程序开始在另一个核心上运行,也许是一个需要大量内存的视频流应用。这个新的工作负载可能会开始填满LLC,并在此过程中,可能会意外地驱逐恰好持有锁的那个缓存行。由于严格的包容性规则,这次从LLC的驱逐会触发反向无效,从而清除所有等待核心中的锁缓存行。这种干扰减慢了锁的交接过程,降低了同步应用的性能。包容性缓存在试图管理一切的同时,却允许了系统一部分的扰动破坏了另一部分的关键操作。

机器中的幽灵:包容性如何制造安全漏洞

缓存包容性最惊人的后果在于安全领域。在这里,提供秩序和效率的特性被扭曲成了间谍工具。包容性的机制本身——目录和反向无效——创造了可观察的副作用,恶意程序可以通过测量这些副作用来窃取信息。

考虑反向无效机制。它建立了一个因果链:从LLC中驱逐一个行导致它在任何私有缓存中失效。攻击者可以利用这一点。攻击者在一个核心上运行,可以策略性地访问数据以填满共享LLC的特定部分,从而故意驱逐受害者程序在另一个核心上使用的缓存行。由此产生的反向无效实际上让攻击者得以伸入受害者的私有缓存并移除数据。这就是​​Flush+Reload​​攻击的基础。攻击者“冲刷”(flush)一个共享数据行并等待。然后,它“重载”(reload)它。如果重载速度快,说明受害者没有访问它。如果重载速度慢,说明受害者访问了它,从而迫使从主存中获取。包容性策略使得这种攻击极其有效,因为“冲刷”步骤——从LLC中驱逐——保证了也会从受害者的私有缓存中清除该行。

当与现代CPU的另一个特性——推测执行——相结合时,情况变得更加危险。为了提高速度,处理器会“猜测”接下来要执行哪些指令。如果猜测错误,结果会被丢弃,但微体系结构上的副作用——比如对缓存的更改——通常会保留下来。这些被称为瞬态执行,是从未正式发生的计算的幽灵。

当受害者程序瞬态执行一条加载数据的指令时,它可能会将该数据带入其L1缓存。在包容性策略下,这个动作并非私密。为了维持包容性,共享LLC中必须发生相应的变化——要么首次为该行分配空间,要么更新其状态。这意味着受害者代码中的每一次瞬态执行都会在共享LLC中留下一个可靠的足迹,一个可观察的信号。攻击者可以使用​​Prime+Probe​​技术来检测这些微小的变化,从而有效地观察受害者执行的幽灵般的回声,并推断出秘密数据,如加密密钥。包容性策略就像一个放大器,使得这些微弱、瞬态的信号变得响亮而清晰,让攻击者得以听到。从这个角度看,推测性填充留下的被浪费的标签条目不仅仅是效率低下,更是一个潜在的信息泄露点。

更广阔的视角:系统、算法及其他

包容性原则的触角远远超出了单个芯片的范围,影响着大规模云计算系统的设计,甚至数值算法的结构本身。

在​​虚拟化云环境​​中,一个常见的任务是将一个正在运行的虚拟机(VM)从一台物理服务器迁移到另一台——即“实时迁移”。为了在不长时间暂停的情况下完成此操作,系统需要知道VM在处理器缓存中存储了哪些数据。包`容性LLC在这方面是一个巨大的优势;虚拟机监控程序可以简单地查询LLC以获得完整的快照。没有它,这个过程要复杂得多。然而,这个好处是有代价的。随着越来越多的VM被打包到单个服务器上,它们都在争夺共享LLC中的空间。包容性策略固有的数据重复加剧了这种压力,导致更高的未命中率和更慢的性能。因此,系统架构师必须权衡一个利弊:使用包容性缓存实现更容易的迁移,还是使用非包容性缓存获得高密度下的更好性能。

现代处理器的复杂性源于许多不同特性的相互作用。例如,硬件预取器试图猜测程序下一步将需要什么数据,并提前将其取入缓存。但如果一个核心上的预取器猜错了会发生什么?在一个包容性系统中,它可能会用无用的数据填满LLC。这种“预取污染”可能会挤掉属于另一个核心的有用数据。由此产生的驱逐会触发一次反向无效,损害一个无辜旁观者程序的性能。这是一个完美的例子,说明两个各自旨在提高性能的特性如何合谋降低性能。

最后,这个硬件细节真的能影响纯数学吗?绝对能。高性能计算依赖于设计“缓存感知”的算法,如​​Tall-Skinny QR (TSQR)​​分解。它们对数据块进行操作,块的大小被设计为能完美地装入处理器的快速内存中。然而,这个快速内存的有效大小取决于缓存策略。包容性策略通过要求数据重复,实际上缩小了缓存可以容纳的唯一数据量。算法设计者必须考虑到这一点。选择包容性策略还是排他性策略可以改变算法本身的最优结构,决定是应该分几个大块处理数据,还是分许多小块处理。硬件设计选择跨越了学科界限,塑造了软件以及它所实现的数学本身。

从一条简单的规则——小缓存里的东西必须在大缓存里——我们一路走来,穿越了处理器效率、软件性能、网络安全漏洞和算法设计。包容性缓存原则证明了计算机系统美丽而复杂的相互关联性。它提醒我们,在追求性能的道路上,没有简单的选择,只有权衡,而理解一个微小决定的后果可以照亮一片广阔而迷人的景象。