try ai
科普
编辑
分享
反馈
  • 非写分配高速缓存策略

非写分配高速缓存策略

SciencePedia玻尔百科
核心要点
  • 写分配在发生写未命中时将数据调入高速缓存,以期未来能够重用;而非写分配则绕过高速缓存,以最小化初始内存流量。
  • 对于流式工作负载(例如视频编码)和稀疏写入,非写分配非常高效,因为它通过避免不必要的数据读取,可将内存流量减少一半。
  • 对于具有高时间局部性的数据,写分配更为优越,因为获取缓存行的初始成本会被后续的快速缓存命中证明是合理的。
  • 现代处理器采用自适应策略,例如用于I/O的写合并内存类型,从而为给定任务动态应用最高效的策略。

引言

在对计算速度不懈追求的道路上,内存高速缓存是现代处理器设计中至关重要的一大支柱。它充当一个高速缓冲区,将频繁使用的数据存放在靠近CPU的位置,以避免访问主内存的漫长过程。虽然处理读未命中的策略很直接——获取数据即可——但对于写未命中,一个更微妙且影响深远的问题出现了:当CPU需要写入一个当前不在高速缓存中的内存位置时,最有效的做法是什么?这一基本设计选择催生了两种截然不同的理念:​​写分配​​(write-allocate)策略,即在写入前将数据调入高速缓存;以及​​非写分配​​(no-write-allocate)策略,即完全绕过高速缓存。这一决策在前期成本和未来收益之间形成了有趣的权衡,对系统性能和内存带宽产生了深远影响。

本文将探讨这一关键选择背后的深层逻辑。在第一章​​原理与机制​​中,我们将剖析两种策略的逐步操作,量化它们在流式写入和稀疏写入等不同工作负载下的性能权衡。随后,我们将在​​应用与跨学科联系​​中转向现实世界,考察这一决策如何影响视频编码、设备通信和多核系统的效率,揭示现代处理器如何巧妙地结合这两种策略以实现最佳性能。

原理与机制

想象你是一位画家,你的高速缓存就是你的调色板。你常用的大部分颜色已经备在调色板上,随时可以快速蘸取——这就是一次高速缓存命中,快速而高效。但当你需要一个调色板上没有的颜色时,比如说,一种特定色调的蔚蓝色,会发生什么呢?这就是一次高速缓存未命中。你有一个选择。你是去你的颜料管(主内存)那里,在调色板的空白处挤上大量的蓝色,然后再把它涂到画布上?还是直接将画笔伸入颜料管中,仅为这一笔蘸取颜料,而让你的调色板保持原样?

这个简单的选择,正是计算机体系结构中一个深刻而重要的设计决策的核心:在​​写分配​​和​​非写分配​​策略之间做出选择。这是一个关于权衡的经典故事,一个关于何时应该有备无患,何时又应该便宜行事的故事。让我们来探究这一选择背后美妙的逻辑。

标准方法:使用写分配准备调色板

最直观的策略,也常常是首先被教授的策略,是​​写分配​​。其理念很简单:如果处理器需要处理一份数据,无论处理器是想读取还是写入,这份数据都应该首先被调入高速缓存。这使得高速缓存的模型保持一致和简单。

让我们来追踪一条未命中高速缓存的 STORE 指令的完整过程。假设你的CPU想要向一个内存地址写入8字节的数据,但该数据对应的64字节“容器”——即​​缓存行​​——当前不在一级(L1)高速缓存中。在写分配策略下,事件序列如下:

  1. ​​未命中并腾出空间​​:CPU发现该行不在L1高速缓存中。如果新行应该存放的高速缓存组已满,就必须选择一个“被替换行”进行驱逐。如果这个被替换行包含已修改的数据(即它是“脏”的),就不能直接丢弃。它的内容必须首先被写回到下一级内存(如L2高速缓存或主内存),以确保没有数据丢失。这是一个​​写回​​操作。

  2. ​​声明所有权​​:随后,L1高速缓存向内存层级下方发送一个特殊请求。这不仅仅是一个简单的读请求,而是一个​​请求所有权的读取(Read-For-Ownership, RFO)​​。一个RFO请求就好比在说:“我不仅需要这个64字节行的副本,我还打算修改它。请给我一个独占副本,并使其他地方可能存在的任何副本无效。”这是确保系统不同部分不会出现同一数据相互冲突版本的一个关键步骤。

  3. ​​填充高速缓存​​:L2高速缓存或主内存响应请求,将整个64字节行发送到L1高速缓存。

  4. ​​执行写入​​:既然数据的容器终于放到了调色板上,CPU便执行其8字节的写入操作。这次写入现在是一次高速缓存命中。该行被更新并标记为“脏”,表示L1高速缓存持有比主内存中更新版本的数据。

这个过程稳健且合乎逻辑。它确保了高速缓存始终是主要的工作区。但看看其中涉及的所有步骤——驱逐、写回、一次完整的64字节读取,然后才是写入。所有这些工作总是必要的吗?

极简主义者的选择:非写分配

如果CPU只是将一个大文件写入磁盘,或者将一大块内存清零呢?它在写入数据,但并没有立即读回这些数据的计划。在这种情况下,仅仅为了覆写,就将整个旧的缓存行调入L1高速缓存似乎……很浪费。

这就是​​非写分配​​(no-write-allocate)策略(有时也称为​​写绕过​​,write-around)背后的洞见。这里的理念是将写未命中作为一种特殊情况来处理。你不是去准备调色板,而是直接将画笔伸入颜料管。

让我们用这个新策略重新审视 STORE 未命中的场景:

  1. ​​未命中并绕过​​:CPU发现该行不在L1高速缓存中。
  2. ​​转发写入​​:L1高速缓存控制器不发起RFO,而是“让到一边”,直接将8字节的写请求转发到内存层级的下一级。

就是这样。L1高速缓存的内容完全保持不变。没有驱逐,没有RFO,没有64字节的缓存行填充。其优雅之处在于它的极简主义。它避免了用可能不会很快再次需要的数据污染高速缓存,为更重要的数据保留了宝贵的高速缓存空间。

量化权衡:何时极简主义更优?

那么,哪种策略更优越?这正是计算机体系结构真正魅力所在。没有唯一的“最佳”答案;正确的选择完全取决于内存访问的模式。这是一场关于预测和优化的迷人游戏。

流式工作负载:“非写分配”的明显胜利

考虑​​流式存储​​的情况,即处理器从头到尾向一个大的、连续的内存块写入数据——比如保存一个视频文件或运行一个输出庞大数组的科学模拟。

让我们分析写入总共 SSS 字节数据所需的内存流量。

  • 使用​​写分配​​,对于该块内的每一个缓存行,处理器首先发出一个RFO来从内存中读取旧的、即将被覆写的行。修改之后,这个脏行最终会被写回。这意味着对于 SSS 字节的有效数据,我们产生了 SSS 字节的读流量和 SSS 字节的写流量,总共在内存总线上传输了 ​​2S2S2S 字节​​。

  • 使用​​非写分配​​,处理器只是将 SSS 字节的新数据发送到内存。没有读取操作。总流量仅为 ​​SSS 字节​​。

结论是惊人的:对于这种极为常见的工作负载,写分配产生的内存流量是其两倍!在一个以内存带宽为瓶颈的系统中,这可以直接转化为非写分配策略​​2倍的性能提升​​。

重用数据:“写分配”的反击

但故事并非如此简单。如果一个程序写入一个内存位置,然后在很短的时间后,又再次写入同一个位置呢?这种被称为​​时间局部性​​的模式也非常普遍。

  • 使用​​写分配​​,第一次写入是昂贵的。它支付了RFO的全部“入场费”将行调入高速缓存。但对同一行的第二次、第三次和第四次写入现在是闪电般快速的高速缓存命中,不产生任何到主内存的流量。

  • 使用​​非写分配​​,第一次写入是廉价的——只是一个发送到内存的小写入。但第二次、第三次和第四次写入也是未命中,必须发送到内存。它避免了入场费,但每次访问都要支付“过路费”。

这里有一个明确的权衡:写分配为未来可能获得的廉价访问付出了高昂的前期成本,而非写分配则以牺牲所有后续访问为代价,最小化了第一次访问的成本。我们可以对此进行建模,并找到一个“临界重用概率”,在该概率下一种策略会优于另一种。如果很快再次写入同一行的概率足够高,写分配策略的初始投资将获得丰厚的回报。

稀疏写入:杀鸡用牛刀的代价

非写分配大放异彩的另一个场景是​​稀疏写入​​,即程序在一个非常大的内存区域中零星地修改几个字节。

在写分配策略下,即使只写入一个字节,也需要一个RFO来获取整个64字节的行。这就像点了一整张披萨却只吃一块意大利辣香肠——从内存中读取的绝大部分数据都是“浪费的带宽”,因为它们被取来只是为了立即被覆写或忽略。相比之下,非写分配通过只发送实际被修改的特定字节来优雅地处理这种情况,产生的流量要少得多。

实现的艺术:改进与自适应

现代处理器是工程学的杰作,它们采用了一些巧妙的技巧来兼得两者的优点。

  • ​​写合并​​:使用非写分配的处理器通常采用​​写合并缓冲区​​。如果CPU在短时间内对同一个缓存行发出几次小的写入,这个缓冲区不会将每一次都单独发送到内存,而是将它们收集起来。一旦收集到一整个缓存行的数据(或经过短暂超时后),它会通过一次高效的突发事务将所有数据发送到内存。这就像在结账前等待你的亚马逊购物车装满,从而节省“运费”(总线开销)。

  • ​​整行写入优化​​:即使是写分配策略也可以变得更智能。如果处理器一次性写入整个64字节的缓存行,硬件知道没有“旧”数据需要保留。在这种特殊情况下,它可以跳过RFO的读取部分,实际上只是分配一个行并用新数据填充它。这消除了整行存储中浪费的读流量,使得写分配在某些流式工作负载中更具竞争力。

  • ​​自适应选择​​:最复杂的处理器不必教条地坚守一种策略。它们可以是​​自适应​​的。在每次写未命中时,处理器可以根据情况做出智能选择。它可以分析情况:“从此刻起,每种策略的预期成本是多少?”写分配的成本是RFO读取加上最终的写回。非写分配的成本是立即的写通加上任何未来访问现在会因未命中高速缓存而产生的成本。通过使用性能计数器、指令类型,甚至来自软件的提示,处理器可以动态地选择预期能为当前运行的特定代码带来更低延迟或更少内存流量的策略。

这种动态决策是设计过程的顶峰。它承认没有普遍的真理,只有一系列的权衡。最终的性能并非来自选择一条规则,而是来自构建一个能深刻理解游戏规则的系统,使其能够为遇到的任何情况选择最佳策略。

应用与跨学科联系

我们已经深入了解了高速缓存写策略的内部工作原理,剖析了[写分配](/sciencepedia/feynman/keyword/write_allocate)和非[写分配](/sciencepedia/feynman/keyword/write_allocate)的逻辑机制。但要真正欣赏这些机制的精妙之处,我们必须离开图表和状态机的抽象领域,去观察它们在现实世界中的运作。为什么芯片设计师——或程序员——会关心这样一个看似微不足道的细节?答案在于,正如我们将看到的,这个选择会产生深远的影响,从你视频流的流畅度到大型超级计算机的效率,无所不包。它完美地诠释了一个简单的局部决策如何能产生深远的全局效应。

发送信息的艺术:流式数据

想象你是一名视频编码器,你的工作是逐帧创建一个庞大的视频文件。你正在写出一个长而连续的数据流。一旦一帧的某一部分被写入,你就不打算再读回它;你的工作是把它发送到它在内存中的最终目的地。

现在,思考一下如果你的高速缓存采用[写分配](/sciencepedia/feynman/keyword/write_allocate)(WA)策略会发生什么。当你写入视频文件一个新的64字节片段的第一个字节时,高速缓存会说:“等等!我没有那个数据。”然后它触发一次请求所有权的读取(RFO),尽职地从主内存中取回整个64字节的旧的、无用的数据。它把这些无用数据一路带进它宝贵的工作空间,结果只是为了让你立即用新的视频帧数据覆盖掉它的每一个字节。这好比粉刷一堵墙时,先给旧的、剥落的油漆拍一张详细的照片,冲洗出来,带回房间,然后才打开你的新油漆罐。这是纯粹的、不折不扣的浪费。

这正是视频编码等计算工作负载中所探讨的场景。对于输出的每一个缓存行,WA策略都会使所需的内存带宽加倍:一次用于RFO的读取,以及一次最终脏数据的写回。而非[写分配](/sciencepedia/feynman/keyword/write_allocate)(WNA)策略,结合写通方法和一个名为写合并缓冲区的巧妙特性,则是优雅的解决方案。在写未命中时,它只是说:“这个不用我来保存。”它完全绕过高速缓存,将写操作直接发往内存。写合并缓冲区会收集所有小的写操作,直到一个完整的缓存行准备就绪,然后向内存发送一次高效的突发传输。结果呢?不必要的RFO被消除了,流的内存流量减少了一半,并且高速缓存也免于被仅仅是路过的数据所污染。

这个原则不仅适用于密集的、逐字节的流,也适用于任何“只写”或“即发即弃”的工作负载。无论一个程序是写入缓存行中的每个字节,还是以大步长稀疏地更新元素,只要数据不会很快被读回,获取旧的行就是一种徒劳之举。对于这类任务,WNA是无可争议的效率冠军。

与外部世界对话:I/O和设备通信

计算的世界不仅仅是CPU与内存的对话。它是一个繁忙的生态系统,处理器必须与各种外围设备通信:网卡、图形处理器、存储控制器等等。这种通信通常通过一种称为内存映射I/O(MMIO)的巧妙技巧进行。从CPU的角度来看,它只是在向一个内存地址写入。但实际上,那个地址是外部设备的“门铃”或“信箱”。

当你的电脑通过网络发送一个数据包时,CPU可能会将一个描述符写入一个特定的MMIO地址。这次写入不是存储数据的请求;它是给网卡的命令:“立即发送这个数据包!”缓存这次写入是毫无意义的。你不想保留命令的本地副本;你希望命令被发送出去。

这是非[写分配](/sciencepedia/feynman/keyword/write_allocate)策略的完美应用。现代系统会定义某些内存区域,比如用于PCIe设备的那些区域,并赋予它们“写合并”内存类型。这告诉CPU对该区域的任何存储操作都使用WNA策略。写操作会绕过高速缓存,并被导入一个写合并缓冲区。这个缓冲区充当一个智能的暂存区,将许多小的、连续的命令写入合并成PCIe总线上的一个大的、高效的事务。WNA防止了高速缓存污染,而写合并缓冲区确保了底层硬件被高效利用。这是一个美妙的共生伙伴关系。

这个原则也极大地简化了在有多个参与者时保持内存一致性这个极其复杂的问题。考虑这样一个场景:CPU向内存中的一个缓冲区写入数据,而一个直接内存访问(DMA)引擎——一种无需CPU干预即可移动数据的专用硬件模块——也想写入同一位置。如果CPU使用[写分配](/sciencepedia/feynman/keyword/write_allocate),它的写入会停留在其私有的L1高速缓存中。DMA的写入会进入主内存。现在系统有了两个不同版本的数据!要调和这个问题需要复杂且缓慢的高速缓存一致性协议。

如果对DMA缓冲区使用非[写分配](/sciencepedia/feynman/keyword/write_allocate)策略,情况就简单得多了。CPU的写入不进入高速缓存;它进入写缓冲区。当DMA引擎发起写入时,系统只需检查这个小而明确的写缓冲区中是否有冲突的地址,并取消CPU的待处理写入。一致性问题被限制在可控范围内且易于管理,从而防止了过时的CPU写入可能覆盖新的DMA数据这种竞争条件的发生。

在多核世界中维持秩序

在多核处理器中,保持数据一致性的挑战呈爆炸式增长。如果每个核心都有自己的私有高速缓存,一个核心的写入如何能被其他核心看到?这是高速缓存一致性协议的领域。在这里,写策略的选择再次具有深远的影响。

想象一个场景,一个核心正在初始化一大块任何其他核心都不会接触的私有数据。每一次写入都是针对一个“冷行”——一个不存在于任何高速缓存中的行。使用[写分配](/sciencepedia/feynman/keyword/write_allocate)策略,每次写未命中都会触发一个RFO。该核心必须在共享互连上传播其请求,制造流量并消耗带宽,而这仅仅是为了从内存中获取它即将完全覆盖的数据。对于一个有几十个核心都在试图初始化其数据的系统来说,这会在关键的共享总线上造成不必要的RFO“交通堵塞”。

非[写分配](/sciencepedia/feynman/keyword/write_allocate)策略扮演了一个“好邻居”的角色。对于这些私有的、冷的写入,核心可以简单地将其数据发往内存,而无需分配一个行,也无需广播一个干扰性的RFO。它保持其活动安静,让互连为更有意义的通信保持畅通。这次写入可能仍会被其他核心窥探以维持一致性,但从内存获取数据的昂贵步骤被避免了。在大型系统中,这个简单的策略选择可以显著减少全系统的内存流量,从而提高整体性能。

系统交互的精妙之舞

为了避免我们得出[写分配](/sciencepedia/feynman/keyword/write_allocate)总是反派的结论,理解性能是各组件相互作用的精妙之舞至关重要。WA是“乐观主义者”的策略:它赌的是正在写入的数据很快会被再次读取,所以它急切地将其调入高速缓存。对于许多工作负载来说,这正是正确的赌注,并带来了巨大的性能提升。

然而,这种乐观主义在某些极端情况下可能导致惊人的糟糕后果。考虑一条试图存储16字节数据的单一指令,但其目标地址未对齐,导致8字节落在一个缓存行,另外8字节落在相邻的缓存行。这被称为“跨行存储”。对于非[写分配](/sciencepedia/feynman/keyword/write_allocate)策略,这没什么大不了的;CPU只是向内存子系统发送两次小的8字节写入。但对于[写分配](/sciencepedia/feynman/keyword/write_allocate),结果可能是一场灾难。单一指令触发了两次独立的高速缓存未命中。每次未命中都可能驱逐一个脏行,导致两次64字节的写回到内存。然后,每次未命中又触发一次64字节的RFO来获取两个旧行。在最坏的情况下,一次16字节的存储操作可能产生 64+64+64+64=25664+64+64+64 = 25664+64+64+64=256 字节的内存流量!WNA的极简主义方法在面对此类架构上的“地雷”时,证明了其更强的鲁棒性。

当我们引入其他性能增强特性,如硬件预取器时,这场舞蹈变得更加错综复杂。预取器试图猜测CPU很快会需要什么数据,并提前将其取入高速缓存。当预取器工作得很好时,它就像魔法一样。但当它出错时,它会用无用的数据污染高速缓存。这种污染可能与[写分配](/sciencepedia/feynman/keyword/write_allocate)策略产生险恶的相互作用。一次不准确的预取会从高速缓存中驱逐一个可能有用的行。如果被替换的行恰好是脏的(也许是由于之前的[写分配](/sciencepedia/feynman/keyword/write_allocate)操作),预取器的错误就触发了一次完全不必要的64字节写回到内存。系统在一个领域的助益之举,在另一个领域造成了意想不到的、代价高昂的后果。

两种哲学的故事

归根结底,[写分配](/sciencepedia/feynman/keyword/write_allocate)和非[写分配](/sciencepedia/feynman/keyword/write_allocate)之间的选择代表了两种不同的哲学。[写分配](/sciencepedia/feynman/keyword/write_allocate)是整洁工作室的哲学:它假设你接触的任何数据都应该被带入你的工作空间(高速缓存),因为你很可能会再次处理它。非[写分配](/sciencepedia/feynman/keyword/write_allocate)是极简主义信使的哲学:它认识到有些任务只是发送一个包裹,最好不要用那些只是路过的东西来弄乱工作室。

现代处理器的真正美妙之处在于它不盲目地只遵循一种哲学。它已经学会了兼容并蓄。通过内存类型等机制,软件可以向硬件提供关于其意图的提示。通过将一块内存区域标记为“写合并”,程序员告诉CPU:“这是一个用于设备的即发即弃的信箱。”CPU凭借其智慧,会自动对该区域应用非[写分配](/sciencepedia/feynman/keyword/write_allocate)策略。对于所有其他“普通”内存,它使用其默认的[写分配](/sciencepedia/feynman/keyword/write_allocate)策略,赌的是时间局部性。高性能计算的艺术就在于这种协作,在于理解这些基本的权衡,并引导硬件为手头的任务做出最明智的选择。