Linux内存系列2 - CPU Cache

CPU Cache

今天的CPU比25年前更复杂。那时候,CPU内核的频率与内存总线的频率相当。内存访问只比寄存器访问慢一点。但是,在90年代初期,CPU设计人员增加了CPU内核的频率,但内存总线的频率和RAM芯片的性能并没有成比例增加,这种情况发生了巨大变化。这并不是因为无法构建更快的RAM,如前一节所述。这是可能的,但不经济。与当前CPU内核一样快的RAM比任何动态RAM都要昂贵几个数量级。

如果选择在具有非常小的,非常快速的RAM的机器和具有大量相对快的RAM的机器之间进行,则第二个将总是胜出,因为工作集大小超过小RAM大小时就会访问次级存储比如硬盘。这里的问题是二级存储的速度,通常是硬盘,用于保存工作集中被换出的部分。访问这些磁盘的速度比DRAM访问慢几个数量级。

幸运的是,它不一定是一个全或无的决定。除了大量的DRAM之外,计算机可以拥有少量的高速SRAM。一种可能的实现方式是将处理器地址空间的某个区域专用为包含SRAM,其余部分则用于DRAM。操作系统的任务将是优化分配数据以利用SRAM。基本上,在这种情况下,SRAM相当于处理器寄存器组的扩展。

虽然这是一个可能的实现,但它不可行。忽略将这种SRAM支持的内存的物理资源映射到进程的虚拟地址空间(其本身非常困难)的问题,该实现会需要每个进程通过软件来管理该内存区域的分配。内存区域的大小因处理器而异(即,处理器们具有不同数量的昂贵的SRAM支持的内存)。程序的每个模块都将声明占有一部分快速内存,这将引来额外开销因为需要进行同步相关的操作。总之,快速内存带来的收益将完全被资源管理的开销所吞噬。

因此,与其将SRAM的管理给到操作系统或用户,不如将其变为由处理器透明使用并由处理器processor所管理。在这种模式下,SRAM被用做于主内存中数据(即cache)的临时拷贝,这些数据可能很快会被处理器使用。这是完全可能的,因为程序的代码和数据具有时间和空间局部性temporal and spatial locality。这意味着,在很短的时间内,很有可能重复使用到相同的代码或数据。对于代码这意味着代码中很可能存在循环,因此相同的代码会一遍又一遍地被执行(这就是空间局部性spatial locality的完美情况 )。理想情况下,数据访问也可能分布在一个小区域内。即使短时间内使用的内存不够紧邻,同样的数据还是有很大概率在不久的将来会被重用(时间局部性 temporal locality)。例如,对于代码来说,这意味着在循环中进行了一次函数调用,该函数位于地址空间的某个地方。该函数在内存中可能会比较偏僻,但对该函数的调用将会在短时间内再次发生。对于数据来说,这意味着一次使用的内存总量(工作集大小)在理想情况下是有限的,但是由于RAM 的随机访问特性,所使用的内存并不是紧邻的。意识到这两个局部性是理解CPU caches概念的关键。

只需要一个简单的计算就可以展现出caches在理论上可以达到怎样的高效性。假设访问主存需要200个周期,访问cache内存需要15个时钟周期。然后,如果没有缓存,代码访问到100个数据,每个数据访问100次,将在内存操作上花费2,000,000个周期,如果可以缓存所有数据,则只需要168,500个周期。这是91.5%的优化。

用于高速缓存的SRAM的大小比主存小很多倍。依笔者在工作站上的CPU缓存相关经验,缓存大小总是主内存大小的1/1000(现代:4MB缓存和4GB主内存)。这本身并不是一个问题。如果工作集的大小(当前处理的数据集)小于缓存大小,则无关紧要。但电脑没有理由没有大的主存。工作集一定会大于缓存。对于运行多个进程的系统尤其如此,这种情况下工作集的大小是所有进程和内核的工作集的总和。

解决缓存只有有限大小这个问题,我们需要一个好策略来决定在任何给定时间应该缓存什么。由于并非所有工作集的数据都完全在同一时间使用, 因此我们可以使用技术临时替换缓存中的某些数据。也许这可以在数据实际需要之前完成。这种预取将消除访问主内存的一些成本,因为它与程序的执行是异步的。所有这些技术(不仅于此)可以使缓存变得比看起来更强大。我们将在3.3节讨论它们。一旦所有这些技术都被利用,剩下的就看程序员的了,并帮助处理器进行决策。如何做到这一点将在第6节讨论。

3.1 CPU Caches in the Big Picture

在深入讨论CPU缓存实现的技术细节之前,有些读者可能会觉得首先更详细地看到缓存如何适应现代计算机系统的“大图”(Big Picture)是有用的。

Linux内存系列2 - CPU Cache_第1张图片
Figure 3.1: 最小Cache配置

图3.1显示了最简单的缓存配置。它对应着最早期使用CPU cache的系统的架构。CPU内核不再直接连接到主内存。{ 在更早的系统中,高速缓存被连接到系统总线,就像CPU和主存储器一样。这使用了一个小技巧,并不是比真正的解决方案。}所有的数据加载和存储都必须经过缓存。CPU核心与缓存之间的连接是一种特殊的快速连接。在一个简化的表示中,主存和高速缓存连接到系统总线,该系统总线也可用于与系统的其他组件进行通信。我们引入了系统总线(现代叫做“FSB”); 参见第2.2节。在本节中,我们先忽略北桥; 假定它是存在的,来加快CPU与主存的通信。

尽管过去几十年来计算机已经使用冯诺依曼体系结构,但经验表明,分离用于代码和数据的缓存是有利的。自1993年以来,英特尔一直使用独立的代码和数据缓存,并且从未回头。代码和数据所需的内存区域几乎相互独立,这就是独立缓存更好地工作的原因。近年来出现了另一个优点:大多数常用处理器的指令解码步骤很慢; 高速缓存已经解码的指令可以加速执行,特别是当由于错误预测或不可预测的分支而导致pipeline为空时。

引入缓存后不久,系统变得更加复杂。高速缓存和主存之间的速度差异再次增大,使得另一个级别的高速缓存不得不被添加进来,它比第一级高速缓存更大且更慢。出于经济原因,仅增加第一级缓存的大小不是一种选择。今天,甚至有机器在生产环境中使用了三级缓存。带有这种处理器的系统如图3.2所示。随着单个CPU的内核数量的增加,未来的缓存级别数量可能会增加。

Linux内存系列2 - CPU Cache_第2张图片
图3.2:具有3级缓存的处理器

图3.2展示了三级缓存,并介绍了我们将在文章的后续部分将使用的术语。L1d是一级数据cache,L1i是一级指令cache。请注意,这只是一个示意图; 现实中的数据流从core到主存的过程中不需要经过任何更高级别的cache。CPU设计人员有很大的自由来设计cache的接口。对于程序员来说,这些设计选择是不可见的。

另外,我们有拥有多个core的处理器,每个core可以有多个“线程”。核心和线程之间的区别在于,独立的核心具有所有硬件资源的独立的副本(几乎(早期的多核处理器,甚至具有单独的第二级缓存而没有第三级缓存)。)。核心可以完全独立运行,除非它们在同一时间使用相同的资源,例如与外部的连接。另一方面,线程们共享几乎所有的处理器资源。英特尔的线程实现只为线程提供单独的寄存器,甚至是有限的,还有一些寄存器是共享的。现代CPU的完整概貌如图3.3所示。

Linux内存系列2 - CPU Cache_第3张图片
图3.3:多处理器,多核,多线程

在这个图中我们有两个处理器,每个处理器有两个内核,每个内核有两个线程。线程共享一级缓存。核心(深灰色)具有单独的1级高速缓存。CPU的所有核心共享更高级别的缓存。两个处理器(两个较大的灰色阴影箱)当然不共享任何缓存。所有这些都很重要,特别是当我们在讨论cache对多进程和多线程应用程序的影响时。

3.2 Cache Operation at High Level

要理解使用缓存的成本和节约,我们必须将第2节中关于机器体系结构和RAM技术的知识与前一节中描述的缓存结构相结合。

默认情况下,CPU内核读取或写入的所有数据都存储在缓存中。有些内存区域不能被缓存,但这只是OS实现者需要关注的内容; 它对应用开发者来说是不可见的。还有一些指令允许程序员故意绕过某些特定缓存。这将在第6节中讨论。

如果CPU需要数据字data word,则首先搜索高速cache。显然,缓存不能包含整个主存的内容(否则我们不需要缓存),但由于所有内存地址都是可缓存的,因此每个缓存项都使用数据字在主存中的地址进行标记。这样,对一个地址的读取或写入请求可以在缓存中搜索匹配的标签。这种情况下的地址可以是虚拟地址或物理地址,根据缓存的实现而定。

由于除了实际内存之外,标签还需要空间,因此选择一个字word作为缓存的粒度效率是不高的。对于x86机器上的32位字,标签本身可能需要32位或更多位。此外,由于空间局限性是cache所基于的原则之一,因此不考虑这一点是不好的。由于相邻内存可能一起使用,因此它应该一起加载到缓存中。请记住我们在第2.2.1节中学到的东西:如果RAM模块可以在没有新CAS或甚至RAS的情况下连续传输多个数据字,信号。所以存储在缓存中的条目不是单个单词,而是几个连续单词的“行”。在早期的高速缓存中,这些行长度为32个字节; 现在标准是64字节。如果内存总线是64位宽,这意味着每个缓存行8个传输。DDR有效地支持这种传输模式。

当处理器需要内存内容时,整个缓存行将被加载到L1d中。通过根据高速缓存行大小屏蔽地址值来计算每个高速缓存行的存储器地址。对于64字节的高速缓存行,这意味着低6位被清零。丢弃的位用作缓存行的偏移量。其余的位在某些情况下用于定位缓存中的行和作为标记。实际中地址值分为三部分。对于32位地址,它可能如下所示:

高速缓存行大小为2 O时 ,低位O位用作高速缓存行的偏移量。接下来的S位选择“缓存集”。我们将很快详细讨论为什么集合,而不是单个插槽,用于缓存行。现在理解有2 S组缓存行就足够了。这留下了形成标签的最高32- S - O = T位。这些T位是与每个高速缓存行相关的值,以区分所有 别名 { 具有相同S地址部分的所有高速缓存行都由相同的别名知道。}它们被缓存在同一个缓存集合中。用于寻址缓存集的S位不必被存储,因为它们对于同一组中的所有缓存行是相同的。

当指令修改内存时,处理器仍然必须首先加载高速缓存行,因为没有指令立即修改整个高速缓存行(规则的例外:6.1节所述的写入组合)。写入操作之前的高速缓存线的内容因此必须被加载。高速缓存不可能容纳部分高速缓存行。被写入并且没有被写回到主存储器的高速缓存行被认为是“脏的”。一旦写入,脏标志被清除。

为了能够在缓存中加载新数据,几乎总是首先需要在缓存中腾出空间。从L1d逐出将缓存行向下推入L2(使用相同的缓存行大小)。这当然意味着必须在L2中创建空间。这反过来可能会将内容推入L3并最终进入主内存。每次驱逐越来越昂贵。这里描述的是现代AMD和威盛处理器所偏好的独占高速缓存模型 。英特尔实现了包含性缓存 { 这种推广并不完全正确。少数缓存是独占的,一些包容性缓存具有独占缓存属性。} L1d中的每个缓存行也存在于L2中。因此从L1d驱逐要快得多。有了足够的二级缓存,浪费内存在两个地方的内容的缺点是微乎其微的,并且在驱逐时可以带来回报。独占缓存的一个可能的优点是加载新的缓存行只需要触及L1d而不是L2,这可能会更快。

只要为处理器体系结构定义的内存模型没有改变,CPU就可以随心所欲地管理这些缓存。例如,处理器利用很少或没有内存总线活动并主动将脏高速缓存行写回主内存是完全正常的。x86和x86-64处理器之间,制造商之间甚至同一制造商型号之间的各种高速缓存体系结构都证明了内存模型抽象的力量。

在对称多处理器(SMP)系统中,CPU的高速缓存不能独立工作。所有处理器应该始终都能看到相同的内存内容。这种统一的内存视图的维护称为“缓存一致性”。如果一个处理器只是简单地看自己的缓存和主存,它就不会在其他处理器中看到脏缓存线的内容。提供从另一个处理器直接访问一个处理器的缓存将是非常昂贵和巨大的瓶颈。相反,处理器会检测另一个处理器何时想要读取或写入某个缓存行。

如果检测到写入访问并且处理器在其高速缓存中具有干净的高速缓存行副本,则此高速缓存行标记为无效。未来的引用将需要重新加载缓存行。请注意,在另一个CPU上的读取访问不需要失效,可以很好地保留多个干净的副本。

更复杂的缓存实现允许发生另一种可能性。如果另一个处理器想要读取或写入的高速缓存行当前在第一个处理器的高速缓存中被标记为脏,则需要采取不同的行动。在这种情况下,主存储器已过期,而发出请求的处理器必须从第一个处理器获取高速缓存行内容。通过侦听,第一个处理器注意到这种情况,并自动向请求处理器发送数据。此操作绕过主内存,但在某些实现中,内存控制器应该注意到此直接传输并将更新的高速缓存行内容存储在主内存中。如果访问用于写入第一个处理器,则使其本地缓存行的副本无效。

随着时间的推移,许多高速缓存一致性协议已经被开发出来。最重要的是MESI,我们将在3.3.4节中介绍。所有这些的结果可以总结为几个简单的规则:

  • 脏缓存线不存在于任何其他处理器的缓存中。

  • 清理同一缓存行的副本可以驻留在任意多个缓存中。

如果可以维护这些规则,那么即使在多处理器系统中,处理器也可以高效地使用它们的缓存。所有处理器需要做的是监视彼此的写入访问并将地址与本地缓存中的地址进行比较。在下一节中,我们将详细介绍实施情况,特别是成本。

最后,我们至少应该给出与缓存命中和未命中相关的成本的印象。这些是英特尔为Pentium M列出的数字:

去哪里 周期
寄存器 <= 1
L1D 〜3
L2 〜14
主内存 〜240

这些是以CPU周期测量的实际访问时间。值得注意的是,对于片上二级缓存,很大一部分(可能甚至是大部分)访问时间是由延迟导致的。这是一个物理限制,只能随着缓存大小的增加而变得更糟。只有进程缩小(例如,从Merom 60纳米到英特尔阵容中Penryn的45纳米)才能改善这些数字。

表格中的数字看起来很高,但幸运的是,每次发生缓存加载和丢失都不需要支付全部成本。成本的某些部分可以隐藏起来。当今的处理器都使用不同长度的内部管线,其中指令被解码并准备执行。准备工作的一部分是从内存(或缓存)加载值,如果它们被传输到寄存器的话。如果内存加载操作可以在管道中足够早地启动,则可能会与其他操作并行发生,并且负载的全部成本可能会被隐藏。L1d通常是可能的; 对于一些L2流水线长的处理器也是如此。

尽早开始内存读取有许多障碍。这可能很简单,因为没有足够的资源来访问内存,或者可能由于另一条指令导致负载的最终地址变得迟了。在这些情况下,装载成本无法完全隐藏。

对于写入操作,CPU不一定要等到该值安全地存储在内存中。只要以下指令的执行看起来与将值存储在内存中的效果相同,就没有任何操作可以阻止CPU采取快捷方式。它可以尽早开始执行下一条指令。在影子寄存器的帮助下,这些寄存器可以保存常规寄存器中不再可用的值,甚至可以更改要存储在不完整写入操作中的值。

Linux内存系列2 - CPU Cache_第4张图片
图3.4:随机写入的访问时间

有关缓存行为的影响的示例,请参见图3.4。我们将讨论后来生成数据的程序; 它是以随机方式重复访问可配置数量内存的程序的简单模拟。每个数据项具有固定大小。元素的数量取决于所选的工作集大小。Y轴显示处理一个元素所需的平均CPU周期数; 注意Y轴的刻度是对数的。这种情况在所有这类图表中都适用于X轴。工作集的大小始终以2的幂次显示。

该图显示了三个不同的高原。这并不奇怪:特定处理器具有L1d和L2高速缓存,但没有L3。根据一些经验,我们可以推断L1d的大小为2 13字节,而L2的大小为2 20字节。如果整个工作集适合L1d,则每个元素的每个操作的周期数低于10.一旦超过L1d大小,处理器必须从L2载入数据,并且平均时间会跳到约28个。一旦L2不足时间再次超过480个周期和更多。这是当许多或大多数操作必须从主存储器加载数据时。更糟糕的是:由于数据正在被修改,脏缓存行也必须被写回。

此图应该提供足够的动机来研究有助于改进缓存使用情况的编码改进。我们在这里并不是在谈论几个可怜的百分比。我们正在讨论有时可能的数量级的改进。在第6节中,我们将讨论允许编写更高效代码的技术。下一节将介绍CPU缓存设计的更多细节。本文其余部分的知识是有益的,但不是必需的。所以这部分可以跳过。

3.3 CPU缓存实施细节

缓存实现者有一个问题,即巨大的主存中的每个单元都可能被缓存。如果一个程序的工作集足够大,这意味着有很多主要的内存位置为缓存中的每个地方而战。以前有人指出,高速缓存与主内存大小之比为1比1000并不少见。

3.3.1关联性

有可能实现一个缓存,其中每个缓存行可以保存任何内存位置的副本。这被称为全关联缓存。要访问缓存行,处理器内核必须将每个缓存行的标签与请求地址的标签进行比较。标签将包含地址的整个部分,而不是缓存行的偏移量(也就是说, 3.2节的图中的S为零)。

有像这样实现的缓存,但通过查看当前使用的L2的数字,将显示这是不切实际的。给定具有64B缓存行的4MB缓存,缓存将具有65536个条目。为了获得足够的性能,缓存逻辑必须能够在几个周期内从所有这些条目中选择与给定标签匹配的条目。实施这个的努力将是巨大的。

Linux内存系列2 - CPU Cache_第5张图片
图3.5:完全关联缓存原理图

对于每个高速缓存行,需要一个比较器来比较大标记(注意,S为零)。每个连接旁边的字母以位为单位指示宽度。如果没有给出它是一个单一的位线。每个比较器必须比较两个T位宽的值。然后,根据结果,选择适当的缓存行内容并使其可用。这需要合并尽可能多的 O组数据线,因为有缓存桶。实现单个比较器所需的晶体管数量很大,尤其是因为它必须工作得非常快。没有迭代比较器可用。减少比较数量的唯一方法是通过迭代比较标签来减少它们的数量。这不适用于迭代比较器不同的原因:它需要太长时间。

完全关联的缓存对于小型缓存很实用(例如,某些英特尔处理器上的TLB缓存是完全关联的),但这些缓存很小,非常小。我们最多在谈论几十个条目。

对于L1i,L1d和更高级别的缓存,需要采用不同的方法。可以做的是限制搜索。在最极端的限制中,每个标签只映射到一个缓存条目。计算很简单:考虑到具有65,536个条目的4MB / 64B高速缓存,我们可以通过使用地址(16位)的位6至21直接对每个条目进行寻址。低6位是进入缓存行的索引。

Linux内存系列2 - CPU Cache_第6张图片
图3.6:直接映射缓存原理图

这种直接映射的缓存如图3.6所示,实施起来速度快且相对容易。它只需要一个比较器,一个多路复用器(在这个图中两个标签和数据是分开的,但这对设计来说不是一个硬性要求),还有一些逻辑只选择有效的缓存行内容。由于速度要求,比较器很复杂,但现在只有其中一个; 因此可以花费更多的精力来加快速度。这种方法的真正复杂性在于复用器。简单多路复用器中的晶体管数量随O(log N)增加,其中N是缓存线的数量。这是可以忍受的,但可能会变慢,在这种情况下,速度可以通过在多路复用器中的晶体管上花费更多的空间来并行化一些工作并提高速度来增加。随着缓存容量的增加,晶体管的总数量可能会增长缓慢,这使得该解决方案非常吸引人。但是它有一个缺点:只有当程序使用的地址相对于用于直接映射的位均匀分布时,它才能很好地工作。如果它们不是,并且通常是这种情况,则一些高速缓存条目被大量使用,并因此被反复驱逐,而其他条目几乎不被使用或保持空着。

Linux内存系列2 - CPU Cache_第7张图片
图3.7:组关联缓存原理图

这个问题可以通过使缓存集关联来解决。集合关联缓存结合了完整关联缓存和直接映射缓存的特性,在很大程度上避免了这些设计的弱点。图3.7显示了一个关联缓存的设计。标签和数据存储被分成由地址选择的组。这与直接映射缓存类似。但是,对于缓存中的每个设置值只有一个元素,而对于相同的设置值,缓存少量值。并行比较所有集合成员的标签,这与全关联缓存的功能类似。

结果是一个缓存不容易被不幸的或故意选择的具有相同设置数的地址所破坏,同时缓存的大小不受可并行实现的比较器的数量的限制。如果缓存增长,则(在该图中)只有增加的列数,而不是行数。只有缓存的关联性增加时,行数才会增加。今天,处理器对L2高速缓存或更高级别使用高达16的关联级别。L1缓存通常与8。

Linux内存系列2 - CPU Cache_第8张图片
Table 3.1: Effects of Cache Size, Associativity, and Line Size

考虑到我们的4MB / 64B高速缓存和8路集合相关性,我们剩下的高速缓存拥有8,192个集合,并且只有13个标记位用于处理高速缓存集。为了确定高速缓存集合中的哪些(如果有的话)条目包含被寻址的高速缓存行8标签必须被比较。这在很短的时间内是可行的。通过实验我们可以看出这是有道理的。

表3.1显示了一个程序的二级高速缓存未命中的数量(根据Linux内核人员的情况,在这种情况下,它们是最重要的基准),用于更改高速缓存大小,高速缓存行大小和关联集大小。在第7.2节中,我们将介绍用于模拟测试所需的缓存的工具。

以防万一这还不明显,所有这些值的关系是缓存大小

缓存行大小×关联度×集数

地址通过使用映射到缓存

O = log 2缓存行大小
S = log 2组数

如3.2节所示。

Linux内存系列2 - CPU Cache_第9张图片
图3.8:缓存大小vs关联性(CL = 32)

图3.8使得表格的数据更易于理解。它显示了32字节的固定高速缓存行大小的数据。通过查看给定缓存大小的数字,我们可以看到,关联确实可以帮助显着减少缓存未命中的次数。对于从直接映射到双向联合缓存的8MB缓存,几乎可以节省44%的缓存未命中。与直接映射缓存相比,处理器可以使用组关联缓存保留缓存中的更多工作集。

在文献中偶尔可以看到,引入关联性与缓存大小加倍相同。在一些极端情况下,从4MB到8MB缓存的跳转可以看出这一点。但是,进一步加倍联合性肯定不是事实。正如我们在数据中看到的那样,连续的收益要小得多。不过,我们不应该完全打折。在示例程序中,峰值内存使用量为5.6M。因此,对于8MB缓存,对于相同的缓存集不太可能有多个(多于两个)使用。使用更大的工作集时,节省可能会更高,因为我们可以从更小的缓存大小的关联性的更大好处中看出。

一般来说,增加8以上缓存的关联性似乎对单线程工作负载影响不大。随着采用共享L2的多核处理器的推出,情况发生了变化。现在你基本上有两个程序打在同一个缓存上,导致实际中的关联性减半(或四分之一核心处理器)。所以可以预料,随着内核数量的增加,共享缓存的关联性会增加。一旦这种情况不再可能(16路集相关性已经很难了),处理器设计人员必须开始使用共享的L3缓存,而L2缓存可能会被一部分内核共享。

我们可以在图3.8中研究的另一个影响是缓存大小的增加如何帮助提高性能。如果不知道工作集的大小,则无法解释这些数据。显然,与主内存一样大的缓存会导致比较小的缓存更好的结果,所以通常对最大缓存大小没有限制,并具有可测量的益处。

正如上面已经提到的那样,工作集的峰值大小是5.6M。这不会给我们任何绝对数量的最大有益缓存大小,但它允许我们估计数量。问题是,并非所有使用的内存都是连续的,因此,即使使用16M缓存和5.6M工作集,也会产生冲突(请参阅双向组关联16MB缓存相对于直接映射版本的优势) 。但是可以肯定的是,在相同的工作负载下,32MB缓存的好处可以忽略不计。但谁说工作集必须保持不变?随着时间的推移,工作量在不断增加,缓存大小也应该增加。购买机器时,必须选择愿意支付的缓存大小,因此测量工作集大小是值得的。为什么这很重要可以在图3.10的数字中看到。

Linux内存系列2 - CPU Cache_第10张图片
图3.9:测试内存布局

运行两种类型的测试。在第一次测试中,元素按顺序处理。测试程序遵循指针n, 但数组元素被链接在一起,以便它们按照在内存中找到的顺序遍历。这可以在图3.9的下半部分看到。有一个来自最后一个元素的引用。在第二个测试中(图的上半部分),数组元素以随机顺序遍历。在这两种情况下,数组元素形成一个循环的单链表。

3.3.2缓存效果的度量

所有数字都是通过测量一个程序来创建的,该程序可以模拟任意大小的工作集,读取和写入访问以及顺序或随机访问。我们已经在图3.4中看到了一些结果。该程序将创建一个与此类型元素的工作集大小相对应的数组:

  struct l {
    struct l * n;
    长整形垫[NPAD];
  };

所有条目都使用n元素链接在循环列表中,无论顺序还是随机顺序。从一个入口前进到下一个入口始终使用指针,即使这些元素是按顺序排列的。该元素是有效载荷,它可以尽量增大。在一些测试中,数据被修改,而在其他测试中,程序仅执行读取操作。

在性能测量中,我们正在讨论工作集大小。工作集由一组struct l 元素组成。一个2 N字节的工作集包含

2 N / sizeof(struct l)

元素。显然sizeof(struct l)取决于NPAD的值 。对于32位系统,NPAD = 7表示每个数组元素的大小为32个字节,对于64位系统,大小为64个字节。

单线程顺序访问

最简单的情况是简单地遍历列表中的所有条目。列表元素按顺序排列,密集排列。无论处理顺序是向前还是向后都无所谓,处理器可以很好地处理两个方向。我们在这里测量的以及在所有以下测试中测量的是处理单个列表元素需要多长时间。时间单位是一个处理器周期。结果如图3.10所示。除非另有说明,所有测量均在64位模式下,这意味着结构的奔腾4的机器上制成NPAD = 0是在大小的8个字节。

Linux内存系列2 - CPU Cache_第11张图片
图3.10:顺序读取访问,NPAD = 0
Linux内存系列2 - CPU Cache_第12张图片
图3.11:几种尺寸的顺序读取

前两个测量值受噪声污染。测量的工作量太小,无法过滤系统其余部分的影响。我们可以安全地假设这些值都处于4个周期的水平。考虑到这一点,我们可以看到三个不同的层次:

  • 最大工作集大小为2

    14

    字节。

  • 从2

    15

    字节到2

    20

    字节。

  • 从2

    21

    个字节起。

这些步骤可以很容易地解释:处理器具有16kB L1d和1MB L2。我们没有看到从一个级别转换到另一个级别的尖锐边缘,因为缓存也被系统的其他部分使用,因此缓存并非专门用于程序数据。具体来说,二级缓存是一个统一的缓存,也用于说明(注意:英特尔使用包含缓存)。

可能不是所期望的是不同工作集规模的实际时间。预计L1d命中次数:P4命中L1d命中后的负载时间大约为4个周期。但是L2访问呢?一旦L1d不足以保存数据,人们可能认为每个元素需要14个周期或更多,因为这是L2的访问时间。但结果显示只需要约9个周期。这种差异可以通过处理器中的高级逻辑来解释。为了使用连续的存储区域,处理器预取下一个缓存行。这意味着当下一行被实际使用时,它已经中途加载了。因此,等待下一个缓存行被加载所需的延迟远小于L2存取时间。

一旦工作集大小超过L2大小,预取的效果就更加明显。在我们说主存储器访问需要200+个周期之前。只有有效的预取,处理器才有可能将访问时间保持在9个周期的低位。正如我们从200和9之间的差异所看到的,这很好地解决了问题。

我们可以在预取的同时观察处理器,至少是间接的。在图3.11中,我们看到了相同工作集大小的时间,但是这次我们看到了不同大小结构 l的图。这意味着我们在列表中有更少但更大的元素。不同的大小会导致(仍然连续)列表中n个元素之间的距离增大。在图的四种情况下,距离分别为0,56,120和248字节。

在底部,我们可以看到上一个图形中的一行,但这次它或多或少显示为一条扁平线。其他案件的时间则更加糟糕。我们也可以在该图中看到三个不同的级别,并且我们发现测试中的工作集规模较小时会出现大的错误(再次忽略它们)。只要涉及到L1d,线条或多或少都相互匹配。没有必要预取,因此所有元素大小都会在每次访问时触及L1d。

对于L2缓存命中,我们看到三条新线都非常匹配,但它们处于更高的级别(约28)。这是L2的访问时间级别。这意味着从L2预取到L1d基本上是禁用的。即使NPAD = 7,我们也需要为循环的每次迭代创建一个新的缓存行; 对于 NPAD = 0,相反,在需要下一个高速缓存行之前,循环必须迭代八次。预取逻辑无法在每个周期加载新的高速缓存行。因此,我们在每次迭代中都会看到一个从L2加载的失速。

一旦工作集大小超过L2容量,它会变得更有趣。现在所有四条线路差别很大。不同的元素尺寸在性能差异中扮演着重要的角色。处理器应该识别步幅的大小,而不是为NPAD获取不必要的缓存行``= 15和31,因为元素大小小于预取窗口(见6.3.1节)。在元素大小妨碍预取努力的地方是硬件预取限制的结果:它不能跨越页面边界。每增加一个尺寸,我们都会将硬件调度程序的有效性降低50%。如果允许硬件预取器跨越页面边界并且下一页不是驻留或有效的,则操作系统将不得不参与查找页面。这意味着该程序会遇到一个它自己没有启动的页面错误。这是完全不可接受的,因为处理器不知道页面是不存在还是不存在。在后一种情况下,操作系统将不得不中止该过程。无论如何,对于NPAD来说= 7和更高,我们需要每个列表元素的一个缓存行,硬件预取器不能做太多。根本没有时间从内存加载数据,因为所有的处理器都是读一个字,然后加载下一个元素。

经济放缓的另一个重要原因是TLB缓存未命中。这是一个缓存,其中存储了将虚拟地址转换为物理地址的结果,如第4节中详细介绍的那样.TLB缓存非常小,因为它必须非常快速。如果反复访问更多的页面比TLB缓存具有从虚拟地址到物理地址的翻译必须不断重复的条目。这是一个非常昂贵的操作。对于更大的元素大小,TLB查找的成本会在更少的元素上分摊。这意味着每个列表元素必须计算的TLB条目总数更高。

要观察TLB效应,我们可以运行不同的测试。对于一次测量,我们按照惯例顺序排列元素。对于占用一个整个缓存行的元素,我们使用 NPAD = 7。对于第二次测量,我们将每个列表元素放置在单独的页面上。每个页面的其余部分保持不变,我们不会将其计入工作集大小的总数中。{ 是的,这有点不一致,因为在其他测试中,我们在元素大小中计算结构中未使用的部分,我们可以定义NPAD以便每个元素填充一个页面。在这种情况下,工作集的大小将会非常不同。不过这并不是测试的要点,而且由于预取无效,所以这没什么区别。}结果是,对于第一次测量,每个列表迭代都需要一个新的缓存行,并且对于每64个元素来说,一个新页面。对于第二次测量,每次迭代都需要加载新页面上的新缓存行。

Linux内存系列2 - CPU Cache_第13张图片
图3.12:顺序读取的TLB影响

结果可以在图3.12中看到。测量在与图3.11相同的机器上进行。由于可用RAM的限制,工作集大小必须限制在2 24个字节,这需要1GB将对象放置在不同的页面上。较低的红色曲线完全对应于图3.11中的 NPAD = 7曲线。我们看到显示L1d和L2高速缓存大小的不同步骤。第二条曲线看起来完全不同。重要的特征是当工作集合大小达到2 13时开始的巨大峰值字节。这是TLB缓存溢出的时候。使用64字节的元素大小,我们可以计算出TLB缓存有64个条目。没有页面错误影响成本,因为程序锁定内存以防止它被换出。

可以看出,计算物理地址并将其存储在TLB中所需的周期数非常高。图3.12中的图表显示了极端情况,但现在应该清楚,较大NPAD值减速的一个重要因素是TLB缓存的效率降低。由于在L2或主存储器的高速缓存行可以被读取之前必须计算物理地址,因此地址转换惩罚是存储器访问时间的附加条件。这部分解释了为什么NPAD = 31的每个列表元素的总成本高于RAM的理论访问时间。

Linux内存系列2 - CPU Cache_第14张图片
图3.13:顺序读写,NPAD = 1

通过查看列表元素被修改的测试运行数据,我们可以看到预取实现的更多细节。图3.13显示了三条线。元素宽度在所有情况下都是16个字节。第一行是现在熟悉的列表行,作为基线。标记为“Inc”的第二行只是在进入下一行之前递增当前元素的pad [0]成员。第三行标记为“Addnext0”,取下一个元素的 pad [0]列表元素,并将其添加到当前列表元素的pad [0]成员中。``

天真的假设是“Addnext0”测试运行速度较慢,因为它有更多的工作要做。在前进到下一个列表元素之前,必须加载该元素的值。这就是为什么看到这个测试实际运行是令人惊讶的,对于某些工作集的规模来说,它比“Inc”测试更快。对此的解释是来自下一个列表元素的加载基本上是强制预取。每当程序前进到下一个列表元素时,我们都知道该元素已经在L1d缓存中。因此,只要工作集大小适合L2高速缓存,我们就会看到“Addnext0”以及简单的“Follow”测试。

不过,“Addnext0”测试的运行速度比“Inc”测试快。它需要从主内存加载更多的数据。这就是为什么“Addnext0”测试达到28个周期级别的工作集大小为2 21字节。28个周期的水平是“Follow”测试达到的14个周期水平的两倍。这也很容易解释。由于其他两个测试修改内存,L2缓存逐出为新缓存线腾出空间不能简单地丢弃数据。相反,它必须写入记忆。这意味着FSB上的可用带宽减半,因此将数据从主存储器传输到L2的时间加倍。

Linux内存系列2 - CPU Cache_第15张图片
图3.14:更大的L2 / L3高速缓存的优势

顺序,高效的缓存处理的最后一个方面是缓存的大小。这应该是显而易见的,但仍需要指出。图3.14显示了具有128字节元素的增量基准测试(64位机器上的NPAD = 15)的时序。这次我们看到来自三台不同机器的测量结果。前两台机器是P4,最后一台是Core2处理器。前两个通过具有不同的缓存大小来区分它们自己。第一款处理器有32k L1d和1M L2。第二个有16k L1d,512k L2和2M L3。Core2处理器具有32k L1d和4M L2。

图中有趣的部分并不一定是Core2处理器相对于其他两个处理器表现得如何(尽管它令人印象深刻)。这里感兴趣的主要问题是工作集大小对于相应的最后一级缓存来说太大并且主存储器被大量占用的区域。

Linux内存系列2 - CPU Cache_第16张图片
Table 3.2: L2 Hits and Misses for Sequential and Random Walks, NPAD=0

正如预期的那样,最后一级缓存越大,曲线停留在对应于L2访问成本的低级别的时间越长。需要注意的重要部分是它提供的性能优势。第二个处理器(稍微老一点)可以在2个20字节的工作集上执行两倍于第一个处理器的工作。这要归功于增加的最后一级缓存大小。带有4M L2的Core2处理器性能更好。

对于随机工作量来说,这可能并不意味着太多。但是,如果工作负载可以根据最后一级缓存的大小进行调整,那么程序性能可以显着提高。这就是为什么有时值得花费额外的钱用于具有更大缓存的处理器的原因。

单线程随机访问测量

我们已经看到,通过预取高速缓存行到L2和L1d,处理器能够隐藏大部分主内存,甚至可以隐藏L2访问延迟。但是,只有当内存访问是可预测的时,这才能正常工作。

Linux内存系列2 - CPU Cache_第17张图片
图3.15:顺序随机读取,NPAD = 0

如果访问是不可预知的或随机的,情况就大不相同。图3.15将顺序访问的每个列表元素时间(与图3.10相同)与列表元素随机分布在工作集中的时间进行比较。该订单由随机化的链接列表确定。处理器无法可靠地预取数据。这只能在偶然使用的元素在内存中彼此靠近的情况下偶然运行。

图3.15中有两点需要注意。首先,大数量是增长工作集规模所需的周期。该机器可以在200-300个周期内访问主存储器,但在这里我们可以达到450个周期以上。我们之前已经看到过这种现象(比较图3.11)。自动预取实际上在这里处于劣势。

第二个有意思的地方在于,曲线在各个平台上并不平坦,正如顺序访问的情况一样。曲线不断上升。为了解释这一点,我们可以测量程序对各种工作集大小的L2访问权限。结果可以在图3.16和表3.2中看到。

如图所示,当工作集大小大于L2大小时,高速缓存失败率(L2未命中/ L2访问)开始增长。曲线与图3.15中的曲线具有相似的形式:曲线快速上升,稍微下降,然后又开始上升。与每个列表元素图的周期有很强的相关性。二级失误率将增长直至最终接近100%。给定一个足够大的工作集(和RAM),随机选取的任何高速缓存行位于L2或正在加载的过程中的概率可以任意减少。

仅仅增加的缓存缺失率就解释了一些成本。但还有另一个因素。查看表3.2,我们可以在L2 /#Iter列中看到每次迭代程序的L2使用总数正在增长。每个工作组的尺寸是之前的两倍。所以,如果没有缓存,我们会期望主内存访问量增加一倍。通过缓存和(几乎)完美的可预测性,我们可以看到顺序访问数据中L2使用的适度增加。增加是由于工作集规模的增加,而没有其他。

Linux内存系列2 - CPU Cache_第18张图片
图3.16:L2d错误率
Linux内存系列2 - CPU Cache_第19张图片
图3.17:Page-Wise随机化,NPAD = 7

对于随机访问,每个元素时间每增加一倍工作集大小就会增加100%以上。这意味着每个列表元素的平均访问时间会增加,因为工作集大小只会增加一倍。这背后的原因是TLB失误率上升。在图3.17中,我们看到NPAD随机访问的成本 ``= 7。只有这一次,随机化被修改。在正常情况下,随机化的整个列表作为一个块(由标记∞指示),其他11条曲线显示以较小块执行的随机化。对于标记为'60'的曲线,每组60页(245.760字节)被单独随机化。这意味着块中的所有列表元素都会在转到下一个块中的元素之前遍历。这具有在任何一个时间使用的TLB条目的数量有限的效果。

NPAD = 7 的元素大小为64个字节,对应于缓存行大小。由于列表元素的随机顺序,硬件预取器不太可能产生任何效果,但绝不仅限于少数几个元素。这意味着L2缓存缺失率与一个块中整个列表的随机化没有显着差异。随着块大小的增加,测试的性能逐渐接近单块随机化的曲线。这意味着后一个测试用例的性能受到TLB失误的显着影响。如果TLB缺失可以降低,则性能显着提高(在一项测试中,我们稍后会看到高达38%)。

3.3.3写行为

在我们开始查看多个执行上下文(线程或进程)使用相同内存时的缓存行为之前,我们必须研究缓存实现的细节。缓存应该是连贯的,并且这个一致性对于用户级代码应该是完全透明的。内核代码是一个不同的故事; 它偶尔需要缓存刷新。

这特别意味着,如果缓存行被修改,则在此时间点之后系统的结果与根本没有缓存并且主内存位置本身已被修改的结果相同。这可以通过两种方式或策略来实现:

  • 直写缓存实现;
  • 写回缓存实现。

直写式高速缓存是实现高速缓存一致性的最简单方法。如果高速缓存行被写入,处理器立即也将高速缓存行写入主存储器。这确保了在任何时候主存储器和高速缓存都是同步的。只要缓存行被替换,缓存内容就可以被丢弃。这个缓存策略很简单,但不是很快。例如,一个程序会重复修改一个局部变量,即使这些数据可能没有在其他地方使用,并且可能是短暂的,也会在FSB上产生大量流量。

回写政策更复杂。这里处理器不会立即将修改后的缓存行写回主内存。相反,缓存线只会被标记为脏。当将来某个时刻从高速缓存中删除高速缓存行时,脏位将指示处理器在当时写回数据,而不是仅放弃内容。

写回缓存有机会显着提高性能,这就是为什么系统中具有体面处理器的大多数内存以这种方式被缓存的原因。处理器甚至可以利用FSB上的空闲容量来存储高速缓存行的内容,然后该行必须撤离。这允许清除脏位,并且当需要缓存中的空间时,处理器可以放弃缓存行。

但是回写实现存在一个重大问题。如果有多个处理器(或核心或超线程)可用并访问相同的内存,则必须确保两个处理器始终都能看到相同的内存内容。如果一个处理器上的高速缓存线是脏的(即,它还没有被写回),而第二个处理器试图读取相同的存储器位置,则读取操作不能仅仅出到主存储器。而是需要第一个处理器的缓存行的内容。在下一节中,我们将看到这是如何实现的。

在我们开始之前,还有两个缓存策略需要提及:

  • 写结合; 和
  • 不可缓存。

这两个策略都用于地址空间中没有真实RAM支持的特殊区域。内核为地址范围设置这些策略(在使用内存类型范围寄存器MTRR的x86处理器上),其余部分自动发生。MTRR也可用于选择直写式还是回写式策略。

写入组合是在诸如图形卡等设备上更经常用于RAM的有限缓存优化。由于设备的传输成本远高于本地RAM访问,因此避免进行太多传输更为重要。如果下一个操作修改下一个单词,则仅仅因为该行中的单词已被写入而传输整个高速缓存行是浪费的。人们很容易想象这是一种常见现象,屏幕上水平相邻像素的存储器在大多数情况下也是邻居。顾名思义,写入组合在高速缓存行被写出之前结合了多次写入访问。在理想情况下,整个缓存行是逐字修改的,并且只有在最后一个字被写入后,缓存行才写入设备。这可以显着提高设备对RAM的访问速度。

最后还有不可缓存的内存。这通常意味着内存位置根本不受RAM支持。它可能是一个特殊的地址,它被硬编码为在CPU之外具有某些功能。对于商品硬件,内存映射地址范围最常见的情况是转换为访问连接到总线(PCIe等)的卡和设备。在嵌入式主板上,有时会找到可用于打开和关闭LED的内存地址。缓存这样的地址显然是一个坏主意。在这种情况下,LED用于调试或状态报告,并且希望尽快查看。PCIe卡上的内存可以在没有CPU交互的情况下更改,因此不应该缓存该内存。

3.3.4多处理器支持

在上一节中,我们已经指出了当多个处理器发挥作用时我们遇到的问题。即使是多核处理器也存在那些不共享的缓存级别(至少是L1d)的问题。

提供从一个处理器到另一个处理器的缓存的直接访问是完全不切实际的。一开始,连接速度不够快。实际的替代方案是在需要时将缓存内容传输到其他处理器。请注意,这也适用于不在同一处理器上共享的缓存。

现在的问题是什么时候这个缓存行转移必须发生?这个问题很容易回答:当一个处理器需要一个在另一个处理器的缓存中用于读取或写入的缓存行时。但是,处理器如何确定另一个处理器缓存中的缓存行是否脏?假设这只是因为高速缓存行由另一个处理器加载将是次优的(最好)。通常,大部分内存访问都是读取访问,并且生成的高速缓存行不会变脏。高速缓存行上的处理器操作很频繁(当然,为什么我们有这篇文章?),这意味着在每次写入访问后广播关于更改后的高速缓存行的信息将是不切实际的。

多年来开发的是MESI缓存一致性协议(修改,独占,共享,无效)。该协议的命名方式是使用MESI协议时,缓存行可以处于的四种状态:

  • 修改:本地处理器已修改缓存行。这也意味着它是任何缓存中唯一的副本。

  • 独占:高速缓存行未被修改,但已知未加载到任何其他处理器的高速缓存中。

  • 共享:高速缓存行未被修改,可能存在于另一个处理器的高速缓存中。

  • 无效:缓存线无效,即未使用。

这个协议多年来从较简单的版本开发而来,这些简单的版本不那么复杂但效率较低。通过这四种状态,可以高效地实现写回缓存,同时还支持在不同处理器上同时使用只读数据。

Linux内存系列2 - CPU Cache_第20张图片
图3.18:MESI协议转换

处理器在监听或窥探其他处理器的工作时不需要太多努力即可完成状态更改。处理器执行的某些操作会在外部引脚上发布,从而使处理器的缓存处理对外可见。相关高速缓存行的地址在地址总线上可见。在以下关于状态及其转换的描述中(如图3.18所示),我们将指出什么时候涉及到公共汽车。

最初所有的缓存行都是空的,因此也是无效的。如果将数据加载到缓存中以将缓存更改写入修改。如果加载数据以读取新状态,则取决于另一个处理器是否也加载了缓存行。如果是这种情况,那么新的状态是Shared,否则是Exclusive。

如果从本地处理器读取或写入修改的高速缓存行,则该指令可以使用当前的高速缓存内容,并且状态不会更改。如果第二个处理器想要从高速缓存行读取,第一个处理器必须将其高速缓存的内容发送给第二个处理器,然后它可以将状态更改为Shared。发送给第二处理器的数据也由存储器存储器中的内存控制器接收和处理。如果没有发生这种情况,缓存行不能被标记为共享。如果第二个处理器想写入高速缓存行,第一个处理器发送高速缓存行内容并在本地将高速缓存行标记为无效。这是臭名昭着的“请求所有权”(RFO)操作。在最后一级缓存中执行此操作,就像I→M过渡相当昂贵。对于直写缓存,我们还需要增加将新缓存行内容写入下一个更高级缓存或主内存所需的时间,从而进一步增加成本。

如果高速缓存线处于共享状态并且本地处理器从中读取,则不需要进行状态更改,并且可以从高速缓存中完成读取请求。如果高速缓存行本地写入高速缓存行也可以使用,但状态更改为修改。它还要求其他处理器中缓存行的所有其他可能副本都标记为无效。因此,写入操作必须通过RFO消息通告给其他处理器。如果第二个处理器请求读取高速缓存行,则不需要发生任何事情。主存储器包含当前数据,并且本地状态已经被共享。如果第二个处理器想要写入高速缓存行(RFO),高速缓存行就被标记为无效。不需要总线操作。

独占状态几乎等于一个关键的区别共享状态:本地的写操作也没有必须要在总线上公布。本地缓存副本被称为是唯一的。这可能是一个巨大的优势,所以处理器会尽量在独占状态而不是共享状态下保留尽可能多的缓存行。后者是在该时刻信息不可用的情况下的后备。独占状态也可以完全省去,不会造成功能问题。由于E→M转换比S→M转换速度快得多,所以只会有性能受损。

从状态转换的这种描述中,应该清楚多处理器操作特有的成本在哪里。是的,填充缓存仍然很昂贵,但现在我们还必须注意RFO消息。无论何时发送这样的消息,事情都会变得缓慢。

有两种情况需要RFO消息:

  • 一个线程从一个处理器迁移到另一个处理器,所有的高速缓存行必须移到新的处理器一次。

  • 在两个不同的处理器中真正需要高速缓存行。{

    对于同一处理器上的两个内核来说,较小的级别也是如此。成本只是小一点。RFO消息可能会多次发送。

    }

在多线程或多进程程序中,总是需要一些同步; 这个同步使用内存来实现。所以有一些有效的RFO消息。他们仍然需要尽可能不经常保存。不过,还有其他RFO消息来源。在第6节中,我们将解释这些情景。Cache一致性协议消息必须分布在系统的处理器之间。只有在系统中的所有处理器都有机会回复该消息时,才能进行MESI转换。这意味着答复可能花费的最长时间决定了一致性协议的速度。{这就是我们现在看到的原因,例如,带有三个插槽的AMD Opteron系统。考虑到处理器只有三个超链接,并且需要一个南桥连接,每个处理器只有一跳。}总线上的碰撞是可能的,NUMA系统中的延迟可能很高,当然纯粹的流量可能会减慢速度。所有重要原因都是为了避免不必要的流量。

还有一个问题与多个处理器相关。这些影响具有很高的机器特定性,但原则上问题始终存在:FSB是共享资源。在大多数机器中,所有处理器都通过一条总线连接到内存控制器(见图2.1)。如果单个处理器可以使总线饱和(通常就是这样),那么共享相同总线的两个或四个处理器将会限制每个处理器可用的带宽。

即使每个处理器都有自己的总线连接到内存控制器,如图2.2所示,仍然有总线连接到内存模块。通常这是一条总线,但即使在图2.2的扩展模型中,对同一内存模块的并发访问也会限制带宽。

对于每个处理器都有本地内存的AMD型号也是如此。是的,所有处理器都可以同时快速访问其本地内存。但多线程和多进程程序 - 至少不时 - 必须访问相同的内存区域才能同步。

并发受限于可用于实现必要同步的有限带宽。程序需要仔细设计,以尽量减少不同处理器和内核对相同内存位置的访问。以下测量结果将显示与多线程代码相关的其他缓存效果。

多线程测量

为了确保在不同处理器上同时使用相同的高速缓存行引入的问题的严重性,我们将在这里查看与之前使用的相同程序的更多性能图。但是,这一次,不止一个线程正在同时运行。测量的是任何线程的最快运行时间。这意味着所有线程完成时完成运行的时间甚至更长。该机器使用四个处理器; 测试最多使用四个线程。所有处理器共享一条总线到内存控制器,并且内存模块只有一条总线。

Linux内存系列2 - CPU Cache_第21张图片
图3.19:顺序读取访问,多线程

图3.19显示了128字节条目顺序只读访问(64位机器上的NPAD = 15)的性能。对于一个线程的曲线,我们可以预期一条类似于图3.11的曲线。测量是针对不同的机器的,因此实际数量会有所不同。

这个图中的重要部分当然是运行多个线程时的行为。请注意,没有修改内存,也没有尝试在走链接列表时保持线程同步。即使不需要RFO消息,并且所有高速缓存行都可以共享,但在使用两个线程时,最高线程的性能会降低18%,而使用四个线程时高达34%。由于不必在处理器之间传输高速缓存行,所以这种下降仅由两个瓶颈中的一个或两个引起:从处理器到存储器控制器的共享总线以及从存储器控制器到存储器模块的总线。一旦工作集大小超过本机中的L3缓存,所有三个线程将预取新的列表元素。

当我们修改记忆时,事情变得更加丑陋。图3.20显示了顺序增量测试的结果。

Linux内存系列2 - CPU Cache_第22张图片
图3.20:顺序增量,多个线程

此图对Y轴使用对数刻度。所以,不要被显然微小的差异所愚弄。对于运行两个线程,我们仍然有大约18%的处罚,现在运行四个线程的处罚现在是93%。这意味着当使用四个线程时,预取流量和写回流量几乎使总线饱和。

我们使用对数刻度来显示L1d范围的结果。可以看出,只要多个线程运行,L1d基本上无效。仅当L1d不足以保持工作集时,单线程访问时间超过20个周期。当多个线程正在运行时,即使使用最小的工作集大小,这些访问时间也会立即生效。

这里没有显示问题的一个方面。用这个特定的测试程序很难衡量。即使测试修改了内存,因此我们必须期待RFO消息,但是当使用多个线程时,L2范围的成本并不高。该程序将不得不使用大量内存,并且所有线程必须并行访问相同的内存。如果没有大量的同步操作,那么这将很难实现,这将主导执行时间。

Linux内存系列2 - CPU Cache_第23张图片
图3.21:随机Addnextlast,多个线程

最后在图3.21中,我们得到了随机访问内存的Addnextlast测试的编号。这个数字主要是为了显示惊人的高数字。在极端情况下,现在大约需要1,500个周期来处理单个列表元素。更多线程的使用更加值得怀疑。我们可以总结一个表中多个线程使用的效率。

#Threads Seq Read Seq Inc 兰德添加
2 1.69 1.69 1.54
4 2.98 2.07 1.65

表3.3:多个线程的效率

下表显示了图3.21中三个图中的最大工作集大小的多线程运行效率。该数字显示了通过使用两个或四个线程,测试程序对于最大工作集大小所带来的最佳加速。对于两个线程,加速的理论极限是2,对于四个线程,4.两个线程的数量并不那么糟糕。但是对于四个线程来说,最后测试的数字表明它几乎不值得超出两个线程。额外的好处是微不足道的。如果我们用图3.21来表示数据有点不同,我们可以更容易地看到这一点。

Linux内存系列2 - CPU Cache_第24张图片
图3.22:通过并行提速

图3.22中的曲线显示了加速因子,即与单线程执行代码相比的相对性能。我们必须忽略最小的尺寸,测量不够准确。对于L2和L3缓存的范围,我们可以看到我们确实实现了几乎线性的加速。我们几乎分别达到2和4的因子。但一旦L3缓存不足以保存工作集,数字就会崩溃。它们碰撞到两个和四个线程的加速是相同的(见表3.3中的第四列)。这就是为什么人们很难找到带有超过四个CPU的插座的主板的原因之一,它们都使用相同的内存控制器。具有更多处理器的机器必须以不同方式构建(请参见第5节)。

这些数字不是通用的。在某些情况下,即使是适合最后一级缓存的工作集也不允许线性加速。事实上,这是常态,因为线程通常不像本测试程序那样解耦。另一方面,可以使用大型工作集并仍然利用两个以上的线程。但是,这样做需要思考。我们将在第6部分讨论一些方法。

特例:超线程

超线程(有时称为对称多线程,SMT)由CPU实现,并且是一种特殊情况,因为各个线程不能真正同时运行。除寄存器集外,它们几乎都共享所有的处理资源。单独的内核和CPU仍然可以并行工作,但是在每个内核上实现的线程受限于此限制。理论上每个内核可以有很多线程,但到目前为止,英特尔的CPU最多每个内核有两个线程。CPU负责时间复用这些线程。尽管如此,这一点没有多大意义。真正的优势在于,当当前运行的超线程延迟时,CPU可以调度另一个超线程。在大多数情况下,这是由内存访问引起的延迟。

如果两个线程在一个超线程核心上运行,则如果两个线程的组合运行时间低于单线程代码的运行时间,则该程序仅比单线程代码更高效 。这可以通过重叠通常会顺序发生的不同存储器访问的等待时间来实现。一个简单的计算显示了缓存命中率达到一定加速的最低要求。

一个程序的执行时间可以用一个简单的模型来近似,只有一个级别的缓存,如下所示(参见[htimpact]):

T exe = N [(1-F mem)T proc + F mem(G hit T cache +(1-G hit)T miss)]

变量的含义如下:

ñ = 指示数量。
F mem = 访问内存的N分数。
G 命中 = 击中缓存的负载的分数。
T proc = 每条指令的周期数。
T 缓存 = 缓存命中的周期数。
T 小姐 = 缓存未命中的周期数。
T exe = 程序的执行时间。

为了使用两个线程有意义,两个线程中的每个线程的执行时间必须至多为单线程代码的一半。任何一方的唯一变量是缓存命中数。如果我们解决了不降低线程执行速度50%或更多所需的最小缓存命中率的公式,我们可以得到图3.23中的图。

Linux内存系列2 - CPU Cache_第25张图片
图3.23:加速的最小缓存命中率

X轴表示单线程代码的缓存命中率G hit。Y轴显示多线程代码所需的缓存命中率。此值永远不会高于单线程命中率,否则,单线程代码也会使用该改进的代码。对于55%以下的单线程命中率,程序在所有情况下都可以使用线程。由于缓存未命中以启用第二个超线程,CPU或多或少处于空闲状态。

绿地是目标。如果线程减速小于50%并且每个线程的工作量减半,则组合运行时可能会小于单线程运行时。对于这里建模的系统(使用P4超线程的数字),对于单线程代码,命中率为60%的程序要求双线程程序的命中率至少为10%。这通常是可行的。但是,如果单线程代码的命中率为95%,那么多线程代码的命中率至少需要80%。这很难。尤其是,这是超线程的问题,因为现在每个超线程可用的有效高速缓存大小(L1d在这里,实际上也是L2等等)被减半。两个超线程都使用相同的缓存来加载其数据。

因此,超线程仅在有限的情况下有用。单线程代码的高速缓存命中率必须足够低,以满足上面的公式和减少的高速缓存大小,新的命中率仍然符合目标。然后才能使用超线程才有意义。在实践中结果是否更快取决于处理器是否足以将一个线程中的等待时间与其他线程中的执行时间重叠。并行化代码的开销必须添加到新的总运行时间中,并且这种额外成本通常不能忽略。

在第6.3.4节中,我们将看到一种技术,线程密切协作,通过公共缓存实现紧密耦合实际上是一种优势。如果只有程序员愿意投入时间和精力来扩展他们的代码,这种技术可以应用于很多情况。

应该清楚的是,如果两个超线程执行完全不同的代码(即,两个线程被OS视为分开的处理器以执行单独的进程),则高速缓存大小确实减半,这意味着高速缓存中的显着增加未命中。除非缓存足够大,否则这样的操作系统调度实践是有问题的。除非机器的工作负载由通过其设计确实可以从超线程中受益的进程组成,否则最好关闭计算机BIOS中的超线程。{ 保持超线程功能的另一个原因是调试。SMT在寻找并行代码中的一些问题方面非常出色。}

3.3.5其他细节

到目前为止,我们谈到地址由三部分组成,标记,集合索引和缓存行偏移量。但是,实际使用的是什么地址?如今所有相关的处理器都为进程提供虚拟地址空间,这意味着有两种不同类型的地址:虚拟地址和物理地址。

虚拟地址的问题在于它们不是唯一的。随着时间的推移,虚拟地址可以引用不同的物理内存地址。不同进程中的相同地址也可能指不同的物理地址。所以最好使用物理内存地址,对吧?

这里的问题是指令使用虚拟地址,这些地址必须借助内存管理单元(MMU)转换为物理地址。这是一个非平凡的操作。在执行指令的管道中,物理地址可能仅在稍后阶段可用。这意味着缓存逻辑必须非常快速地确定内存位置是否被缓存。如果可以使用虚拟地址,则高速缓存查找可以在管道中更早发生,并且在高速缓存命中的情况下可以使存储器内容可用。结果是更多的内存访问成本可能被管道隐藏。

处理器设计人员目前正在为第一级缓存使用虚拟地址标记。这些缓存非常小,可以清除而不会造成太多的痛苦。如果进程的页表树更改,至少需要部分清除缓存。如果处理器具有指定已改变的虚拟地址范围的指令,则可以避免完全刷新。鉴于L1i和L1d缓存的低延迟(〜3个周期),使用虚拟地址几乎是强制性的。

对于包含L2,L3,...缓存的较大缓存,需要物理地址标记。这些缓存具有更高的延迟,虚拟→物理地址转换可以及时完成。由于这些缓存较大(即,当它们被刷新时会丢失大量信息)并且由于主要的内存访问延迟而重新填充它们需要很长时间,因此刷新它们往往会代价高昂。

一般来说,不需要知道这些缓存中地址处理的细节。它们不能改变,影响性能的所有因素通常都应该避免或者与高成本相关。如果大多数已使用的高速缓存行落入同一组中,则溢出高速缓存容量是不利的,并且所有高速缓存都会提早出现问题。后者可以通过虚拟缓存来避免,但用户级进程不可能避免使用物理地址寻址的缓存。唯一需要牢记的细节是,如果可能的话,不要将相同的物理内存位置映射到同一进程中的两个或更多虚拟地址。

缓存的另一个细节,对程序员来说很不感兴趣的是缓存替换策略。大多数缓存首先驱逐最近最少使用(LRU)元素。这总是一个很好的默认策略。随着更大的关联性(并且由于增加更多内核,联想性在未来几年的确可能会进一步增长),维护LRU列表变得越来越昂贵,我们可能会看到采用不同的策略。

至于缓存替换,程序员可以做的并不多。如果缓存使用物理地址标记,则无法找出虚拟地址与缓存集关联的方式。可能是所有逻辑页面中的高速缓存行都映射到相同的高速缓存集,导致大部分高速缓存未使用。如果有的话,操作系统的工作是安排这不会经常发生。

随着虚拟化的出现,事情变得更加复杂。现在甚至操作系统都不能控制物理内存的分配。虚拟机监视器(VMM,又名管理程序)负责物理内存分配。

程序员可以做的最好的做法是a)完全使用逻辑内存页面,并且b)尽可能大地使用页面大小以尽可能多样化物理地址。较大的页面尺寸也有其他好处,但这是另一个话题(参见第4节)。

3.4指令缓存

不仅缓存处理器使用的数据,处理器执行的指令也被缓存。但是,这个缓存比数据缓存的问题少得多。有几个原因:

  • 执行的代码数量取决于所需代码的大小。代码的大小通常取决于问题的复杂程度。问题的复杂性是固定的。

  • 虽然程序的数据处理是由程序员设计的,但程序的指令通常由编译器生成。编译器编写者知道有关良好代码生成的规则。

  • 程序流程比数据访问模式更具可预测性。今天的CPU非常擅长检测模式。这有助于预取。

  • 代码总是具有相当好的空间和时间局部性。

程序员应遵循一些规则,但这些规则主要由如何使用这些工具的规则组成。我们将在第6节讨论它们。这里我们只谈谈指令缓存的技术细节。

自从CPU的核心时钟大幅增加,并且缓存(即使是第一级缓存)和内核之间的速度差异增大后,CPU也一直处于流水线状态。这意味着指令的执行分阶段进行。首先解码指令,然后准备参数,最后执行。这样的流水线可能相当长(对于英特尔的Netburst体系结构,这种流水线可能需要20个阶段)。长管道意味着如果管道停滞(即通过它的指令流中断),需要一段时间才能重新加速。例如,如果下一条指令的位置不能被正确预测,或者如果加载下一条指令需要很长时间(例如,必须从存储器中读取),管道延迟就会发生。

因此,CPU设计人员在分支预测上花费了大量的时间和芯片空间,以便尽可能少地发生流水线延迟。

在CISC处理器上,解码阶段也需要一些时间。x86和x86-64处理器尤其受到影响。因此,近年来,这些处理器不会缓存L1i中的指令的原始字节序列,而是缓存经解码的指令。在这种情况下,L1i被称为“跟踪缓存”。跟踪缓存允许处理器在缓存命中的情况下跳过流水线的第一步,如果流水线停滞,则特别好。

如前所述,L2上的缓存是包含代码和数据的统一缓存。显然这里的代码是以字节序列的形式缓存的,而不是解码的。

为了达到最佳性能,只有几条与指令缓存相关的规则:

  1. 生成尽可能小的代码。为了使用流水线而使用软件流水线需要创建更多代码或使用小代码的开销太高时,也有例外。

  2. 只要有可能,请帮助处理器做出好的预取决定。这可以通过代码布局或显式预取来完成。

这些规则通常由编译器的代码生成来强制执行。程序员可以做几件事情,我们将在第6节中讨论它们。

3.4.1自修改代码

在早期的计算机时代,内存很贵。人们竭尽全力减小节目的规模,为节目数据腾出更多空间。经常部署的一个技巧是随着时间的推移改变程序本身。这种自修改代码(SMC)有时仍然可以找到,目前主要是出于性能原因或安全漏洞。

一般应避免使用SMC。虽然它通常被正确地执行,但是有边界情况,如果不正确,会造成性能问题。显然,被更改的代码不能保存在包含解码指令的跟踪缓存中。但即使未使用跟踪缓存,因为代码根本没有执行(或一段时间),处理器可能会有问题。如果即将到来的指令在进入流水线时发生改变,处理器必须丢掉大量工作并重新开始。甚至有些情况下,处理器的大部分状态必须被抛弃。

最后,由于处理器假定 - 出于简单的原因,并且因为在99.9999999%所有情况下都是这样 - 代码页是不可变的,所以L1i实现不使用MESI协议,而是使用简化的SI协议。这意味着如果检测到修改,就必须做出很多悲观的假设。

强烈建议尽可能避免使用SMC。内存不再是稀缺资源。根据具体需要编写单独的函数而不是修改一个函数会更好。也许有一天SMC的支持可以做成可选的,我们可以通过这种方式来检测利用代码来修改代码。如果必须使用SMC,则写入操作应绕过缓存,以免在L1i中需要的L1d中的数据产生问题。有关这些说明的更多信息,请参阅第6.1节。

通常在Linux上很容易识别包含SMC的程序。所有程序代码在使用常规工具链构建时都是写保护的。程序员必须在链接时执行重要的魔术来创建可执行代码页可写的可执行文件。当发生这种情况时,现代英特尔x86和x86-64处理器具有专用的性能计数器,可以计算自修改代码的使用。在这些计数器的帮助下,即使程序由于放松的权限而成功,也很容易用SMC识别程序。

3.5缓存小姐因素

我们已经看到,当内存访问错过高速缓存时,成本飞涨。有时候这是不可避免的,重要的是了解实际成本以及可以采取哪些措施来缓解问题。

3.5.1高速缓存和内存带宽

为了更好地理解处理器的功能,我们测量了最佳环境下的可用带宽。这种测量特别有趣,因为不同的处理器版本差别很大。这就是为什么这个部分充满了几台不同机器的数据。衡量性能的程序使用x86和x86-64处理器的SSE指令一次加载或存储16个字节。就像在我们的其他测试中一样,工作集从1kB增加到512MB,并且每个周期可以加载或存储多少字节。

Linux内存系列2 - CPU Cache_第26张图片
图3.24:Pentium 4带宽

图3.24显示了64位Intel Netburst处理器的性能。对于适合L1d的工作集大小,处理器能够读取每个周期全部16个字节,即每个周期执行一个加载指令(movaps指令一次移动16个字节)。测试对读取的数据没有任何作用,我们只测试读取指令本身。一旦L1d不够用,性能就会急剧下降到每个周期少于6个字节。2 18的步骤字节是由于DTLB缓存耗尽,这意味着每个新页面需要额外的工作。由于读取顺序预取可以完美地预测访问,并且FSB可以为所有大小的工作集以每个周期大约5.3字节的速度对内存内容进行流式处理。但是,预取的数据不会传播到L1d。这些当然是在真实程序中永远不可能实现的数字。将它们视为实际限制。

写入和复制性能比读取性能更惊人。即使对于小的工作集大小,写入性能也不会超过每个周期4个字节。这表明,在这些Netburst处理器中,英特尔选择为L1d使用Write-Through模式,其性能明显受到L2速度的限制。这也意味着从一个存储器区域复制到另一个不重叠的存储器区域的复制测试的性能不会显着更差。必要的读取操作速度非常快,可以与写入操作部分重叠。写入和复制测量结果最值得注意的细节是L2缓存不够用时的低性能。性能下降到每个周期0.5个字节!这意味着写操作比读操作慢10倍。这意味着优化这些操作对于程序的执行更为重要。

在图3.25中,我们在同一个处理器上看到了结果,但有两个线程在运行,其中一个固定在处理器的两个超线程中。

Linux内存系列2 - CPU Cache_第27张图片
图3.25:带有2个超线程的P4带宽

该图以与前一图相同的比例显示,以说明差异,并且由于测量两个并发线程的问题,曲线有点紧张。结果如预期。由于超线程共享除寄存器以外的所有资源,因此每个线程只有一半高速缓存和可用带宽。这意味着即使每个线程都必须等待很长时间并且可以授予其他线程执行时间,但这并没有什么区别,因为其他线程也必须等待内存。这确实显示了超线程可能使用最糟糕的情况。

Linux内存系列2 - CPU Cache_第28张图片
图3.26:核心2带宽

与图3.24和3.25相比,图3.26和3.27的结果看起来与英特尔酷睿2处理器完全不同。这是一个共享L2的双核处理器,它是P4机器上L2的4倍。这只是解释了写入和复制性能的延迟下降。

有更大的差异。整个工作集范围内的读取性能在每个周期的最佳16字节周围徘徊。2 20字节之后的读取性能下降也是由于DTLB的工作集太大而导致的。实现这些高数字意味着处理器不仅能够预取数据并及时传输数据。这也意味着数据被预取到L1d中。

写入和复制性能也非常不同。处理器没有Write-Through策略; 写入的数据存储在L1d中,并且必要时仅驱逐。这允许写入速度接近每个周期的最佳16字节。一旦L1d不够用,性能就会显着下降。与Netburst处理器一样,写入性能也显着降低。由于高读取性能,这里的差别更大。事实上,即使L2不够用,速度差也会增加到20倍!这并不意味着Core 2处理器表现不佳。相反,他们的表现总是比Netburst核心更好。

Linux内存系列2 - CPU Cache_第29张图片
图3.27:2线程的核心2带宽

在图3.27中,测试运行两个线程,Core 2处理器的两个核心中的每一个都有一个线程。虽然两个线程都访问相同的内存,但不一定完全同步。读取性能的结果与单线程案例没有区别。在任何多线程测试用例中,可以看到几个更多的抖动。

有趣的一点是适用于L1d的工作集大小的写入和复制性能。从图中可以看出,性能与数据必须从主存储器中读取的性能相同。两个线程都争夺相同的内存位置,并且必须发送缓存行的RFO消息。问题在于这些请求没有以L2缓存的速度处理,即使两个内核共享缓存。一旦L1d高速缓存不够用,修改的条目就会从每个核心的L1d刷新到共享的L2中。此时,由于L2缓存满足L1d未命中,并且仅在数据尚未刷新时才需要RFO消息,所以性能显着提高。这就是为什么我们看到这些尺寸的工作组速度降低了50%。

由于一家供应商的处理器版本之间存在显着差异,因此也值得关注其他供应商的处理器的性能。图3.28显示了AMD家族10h Opteron处理器的性能。该处理器具有64kB L1d,512kB L2和2MB L3。三级缓存在处理器的所有内核之间共享。性能测试的结果可以在图3.28中看到。

Linux内存系列2 - CPU Cache_第30张图片
图3.28:AMD系列10h Opteron带宽

关于数字的第一个细节是,如果L1d缓存足够,处理器每周期能够处理两条指令。读取性能每个周期超过32个字节,甚至写入性能也是每个周期18.7个字节。尽管如此,读取曲线很快变平,每个周期2.3个字节的数据相当低。此测试的处理器不会预取任何数据,至少不会有效。

另一方面写入曲线根据各种高速缓存的大小执行。对于L1d的全部大小达到峰值性能,对于L2,每个周期对于L2为6个字节,对于L3为每个周期2.8个字节,并且如果甚至L3不能保存所有数据,则最后每个周期0.5个字节。L1d缓存的性能超过(较旧的)Core 2处理器的性能,L2访问速度相当快(Core 2具有较大的缓存),L3和主内存访问速度较慢。

复制性能不能比读取或写入性能好。这就是为什么我们看到最初由读取性能占主导地位的曲线,随后是写入性能。

Opteron处理器的多线程性能如图3.29所示。

Linux内存系列2 - CPU Cache_第31张图片
图3.29:具有2个线程的AMD Fam 10h带宽

读取性能基本不受影响。每个线程的L1d和L2都像以前一样工作,而L3缓存在这种情况下也不会很好地预取。两条线不会过分强调L3的目的。这个测试中最大的问题是写入性能。线程共享的所有数据必须通过L3缓存。这种共享似乎相当低效,因为即使L3高速缓存大小足以容纳整个工作集,成本也远高于L3接入。将该图与图3.27进行比较,我们看到Core 2处理器的两个线程以共享L2高速缓存的速度运行,适用于适当范围的工作集大小。Opteron处理器的性能水平仅适用于非常小的工作集规模,即使在这种情况下也可以实现

只有

L3的速度比Core 2的L2慢。

3.5.2关键字加载

内存从主内存传输到小于缓存行大小的块中的缓存中。今天64 一次传输,高速缓存行大小为64或128 字节。这意味着每个缓存行需要8或16次传输。

DRAM芯片可以在突发模式下传输这些64位块。这可以填充缓存线,而不需要来自存储器控制器的任何进一步命令以及可能的相关延迟。如果处理器预取缓存行,这可能是最好的操作方式。

如果程序对数据或指令的高速缓存访问存在错误(这意味着,它是强制高速缓存未命中,因为数据首次使用,或容量高速缓存未命中,因为有限的高速缓存大小需要逐出高速缓存行)情况不同。程序继续所需的高速缓存行中的单词可能不是高速缓存行中的第一个单词。即使在突发模式和双倍数据传输速率下,单个64位块也会在明显不同的时间到达。每个块到达4个CPU周期或比以前更晚。如果程序需要继续的字是高速缓存行的第八个字节,则在第一个字到达后,程序必须等待另外30个周期或更长时间。

事情不一定非要这样。内存控制器可以按不同的顺序请求缓存行的单词。处理器可以传送程序等待的 关键字和存储器控制器可以首先请求该字。一旦这个单词到达,程序可以继续执行,而其余的缓存行到达并且缓存还没有处于一致的状态。这种技术被称为第一批和第一批重新启动。

当今的处理器实现了这种技术,但有些情况下这是不可能的。如果处理器预取数据,则不知道关键字。如果处理器在预取操作正在进行的时间内请求高速缓存行,它将不得不等待直到关键字到达而不能影响订单。

Linux内存系列2 - CPU Cache_第32张图片
图3.30:缓存行结束时的关键字

即使进行了这些优化,关键字在缓存行上的位置也很重要。图3.30显示了顺序和随机访问的Follow测试。显示的是在第一个字中追逐的指针与指针在最后一个字中的情况下运行测试的速度减慢。元素大小为64字节,对应于缓存行大小。这些数字相当嘈杂,但可以看出,只要L2不足以保持工作集大小,关键词在结尾处的情况的表现大约慢0.7%。顺序访问似乎受到了一些影响。这与预取下一个缓存行时的上述问题一致。

3.5.3缓存放置

高速缓存与超线程,核心和处理器之间的关系不在程序员的控制之下。但程序员可以确定线程的执行位置,然后缓存与使用的CPU相关变得很重要。

这里我们不会详细讨论何时选择运行线程的内核。我们只会描述程序员在设置线程亲和性时必须考虑的体系结构细节。

根据定义,超线程除了寄存器组之外共享一切。这包括L1缓存。这里没有更多要说的。乐趣开始于处理器的各个核心。每个核心至少拥有自己的L1缓存。除此之外,今天还没有很多共同的细节:

  • 早期的多核处理器具有单独的L2高速缓存并且没有更高的高速缓存。

  • 后来英特尔的型号共享了双核处理器的L2缓存。对于四核处理器,我们必须为每对双核处理单独的L2高速缓存。没有更高级别的缓存。

  • AMD的家庭10h处理器具有独立的L2缓存和统一的L3缓存。

处理器供应商的宣传材料中已经写了很多关于各自型号优点的文章。如果核心处理的工作集不重叠,则单独的L2高速缓存具有优势。这适用于单线程程序。由于今天这仍然是现实,所以这种方法不会表现得太差。但总是有一些重叠。高速缓存都包含公共运行时库最常用的部分,这意味着某些高速缓存空间被浪费了。

与英特尔的双核处理器完全共享L1旁边的所有缓存可以有很大优势。如果在两个核心上工作的线程的工作集显着重叠,则可用缓存总量将增加,并且工作集可以更大,而不会降低性能。如果工作集不重叠英特尔的高级智能高速缓存管理应该防止任何一个内核独占整个高速缓存。

但是,如果两个内核在其各自的工作集中使用大约一半的缓存,则存在一些摩擦。缓存不断必须衡量两个内核的缓存使用情况,并且作为此次重新平衡的一部分执行的逐出可能选择不当。为了看到问题,我们看看另一个测试程序的结果。

Linux内存系列2 - CPU Cache_第33张图片
图3.31:两个进程的带宽

测试程序有一个过程不断读取或写入,使用SSE指令,一个2MB的内存块。选择2MB是因为这是该Core 2处理器的L2缓存的一半大小。这个过程被固定到一个核心,而另一个过程被固定到另一个核心。这第二个进程读取和写入可变大小的内存区域。该图显示了读取或写入每个周期的字节数。显示了四个不同的图表,每个过程读取和写入的组合。读/写图用于后台进程,该进程始终使用2MB工作集进行写入,并将测量的进程设置为可读工作集。

图中有趣的部分是2 20和2 23字节之间的部分。如果两个内核的L2缓存完全分离,我们可以预期所有四个测试的性能将在2 21和2 22之间下降字节,这意味着一旦L2缓存耗尽。如图3.31所示,情况并非如此。对于后台进程写入的情况,这是最明显的。在工作集大小达到1MB之前,性能开始恶化。这两个进程不共享内存,因此进程不会生成RFO消息。这些都是纯粹的缓存驱逐问题。智能缓存处理的问题在于,每个核心的有经验高速缓存大小接近于1MB,而不是每个可用的2MB核心。人们只能希望,如果内核之间共享的缓存仍然是即将到来的处理器的特性,则用于智能缓存处理的算法将得到修复。

拥有带两个L2缓存的四核处理器只是在引入更高级别的缓存之前的一个解决方案。与单独的套接字和双核处理器相比,此设计没有明显的性能优势。两个核心通过同一条总线进行通信,在外部,FSB可见。没有特别的快速数据交换。

未来的多核处理器缓存设计将在更多的层次上。AMD的10h系列处理器开始了。我们是否会继续看到处理器内核子集共享较低级别的缓存仍有待观察。由于高速和经常使用的高速缓存不能在多个内核之间共享,因此需要额外的高速缓存。表现会受到影响。它还需要具有高关联性的非常大的缓存。这两个数字(缓存大小和关联性)都必须随共享缓存的核心数量一起扩展。使用大型L3高速缓存和合理大小的L2高速缓存是合理的折衷。L3缓存速度较慢,但理想情况下不如L2缓存那样频繁使用。

对于程序员来说,所有这些不同的设计在进行调度决策时意味着复杂。人们必须知道机器架构的工作负载和细节才能实现最佳性能。幸运的是,我们支持确定机器架构。接口将在后面的章节介绍。

3.5.4 FSB影响

FSB在机器的性能中扮演着重要角色。缓存内容只能在与内存连接允许的情况下才能存储和加载。我们可以通过在两台只有内存模块速度不同的机器上运行程序来显示多少。图3.32显示了在64位机器上对NPAD = 7 的Addnext0测试的结果(将下一个元素pad [0]元素的内容添加到自己的pad [0]元素中) 。两款机器均配备英特尔酷睿2处理器,第一款采用667MHz DDR2模块,第二款采用800MHz模块(增加20%)。

Linux内存系列2 - CPU Cache_第34张图片
图3.32:FSB速度的影响

这些数字表明,当FSB对于大型工作集规模确实存在压力时,我们确实看到了很大的收益。在这个测试中测得的最大性能增加是18.2%,接近理论最大值。这表明,更快的FSB的确可以弥补大部分时间。当工作集适合高速缓存时(并且这些处理器具有4MB L2)并不重要。必须记住,我们在这里测量一个程序。系统的工作集包含所有同时运行的进程所需的内存。通过这种方式,使用更小的程序很容易可以超过4MB内存或更多。

今天,英特尔的一些处理器支持FSB速度高达1333MHz,这意味着又增加了60%。未来将会看到更高的速度。如果速度很重要,并且工作集规模较大,快速RAM和高FSB速度肯定是物有所值的。但是,必须小心,因为即使处理器可能支持更高的FSB速度,主板/北桥也可能不支持。检查规格至关重要。

继续第3部分(虚拟内存)。

你可能感兴趣的:(Linux内存系列2 - CPU Cache)