【译】每个程序员都应该知道的内存知识-第二部分 CPU缓存

楼主:这是Ulrich Drepper“每个程序员都应该知道的内存知识”的第二部分,第一部分指路

当今的CPU比25年前成熟了许多。在过去,CPU核心的频率和内存总线的频率在一个水平上。访问内存只比访问寄存器慢一点。这一情况在90年代初发生了急剧的变化,这一时期CPU的制造商提升了CPU内核的频率,但内存总线的频率及RAM芯片的性能并没有相应的得到提升。如前章节中所述,这不是因为造不出更快的RAM。造更快的RAM是有可能的,但不划算。要做一个像CPU一样快的RAM比做动态RAM的贵了几个数量级。
如果有这样一个选择:是要一台有小容量内存但内存很快的机器,还是一台有大容量但相对快的机器,这台机器的工作集的大小(working set size)大于那个小容量的内存,但有访问第二级存储媒体如硬盘的代价,人们往往会选第二个。问题是访问第二级存储设备的速度,通常来说是硬盘,这个设备用来存储工作集中被换出的数据。访问这些硬盘比DRAM甚至还要慢上几个数量级。

幸运的是这并不是一个全要或全不要的选择。一台电脑可以在配有一个大容量的DRAM之外,配一块小而高速的SRAM。一种可能的实现是,指定一块处理器的地址空间为SRAM,其它的为DRAM。这样操作系统要做的就是优化数据的分布以利用上SRAM。基本上,SRAM是作为处理器的寄存器扩展集使用。

虽然是一个可能的实现,但它不可行。即使忽略掉对此类SRAM内存的物理资源到处理器的虚拟地址空间的映射问题(这本身就相当难了),这一方式还需要每个处理器在软件中对内存区的分配进行管理。每个处理器的内存区大小可能有差别(即处理器有不同大小的昂贵的SRAM内存)。组成程序的每个模块都要求获取快速内存,这就需要同步,从而带来了额外的成本。简言之,配备高速内存带来的好处被资源管理带来的压力完全抵消了。

因此,SRAM在使用上是透明的且被处理器管理,而不是被操作系统或用户控制。在这一模式下,SRAM用来作主存中有可能马上被处理器使用的数据的暂时副本(换言之,缓存)。这是有可能的,因为程序中的代码和数据有其时空位置性。这意味着在短期内,同样的代码和数据很有可能被重用。对代码来说,很有可能是循环中同样的代码被一再执行(关于空间局限性的完美例子)。数据的访问也被理想地局限在了一小块区域。即使在一小段时间内使用的内存空间不是集中在一起的,同样的数据在不久后再被使用是很有可能的(时间局限性)。对代码来说,举个例子,在一个循环中一个方法被调用 ,而这个方法就在地址空间的某处。这一处可能很远,但访问那个方法是不久就要发生的。对于数据来说,同时要使用到的内存量(工作集大小)是合理被限的,但因为RAM随机访问的特征,被使用到的内存不是在一块的。理解局限性的存在是理解如今我们所用的CPU缓存概念的关键。

一个简单的计算可以揭示理论上来说,缓存可以有多高效。假定访问内存要200个时钟周期,访问内存的缓存是15个时钟周期。使用到100个数据100次的代码,在没有缓存的情况下,将会花费2,000,000个时钟周期在内存访问上,而加上缓存的话仅要168,500个时钟周期(本人理解的计算:100 * 200 * 1+100 * 15 * 99=168,500)。提升了91.5%。

用于缓存的SRAM比主存小很多倍。在作者的经验中,配有CPU缓存的主机上CPU缓存总是在主存的约1/1000大小(目前:4M的缓存和4G的主存)。仅这点来说不构成问题。当工作集大小(正被使用的数据大小)小于缓存大小时,没问题。但电脑配大主存不是没道理的。工作集大小注定比缓存大。对于跑多线程的电脑来说,工作集大小是所有单个线程和内核的工作集总和,这一点尤为明显(工作集大小大于缓存)。

为应对有限的缓存问题,需要一套优秀的策略来决定何时缓存什么数据。因为工作集中不是所有的数据都被同时访问,我们可以使用技术暂时用其它数据来替换缓存中的一些数据。这可能在数据被使用前做好。这一预获取操作可以消除访问主存的一些压力,因为它是与程序的执行异步进行的。使用所有的这些技术,甚至更多,可以使缓存看起来比它本身更大。我们将在3.3节讨论它们。一旦这些技术都用上了,就取决于程序员如何帮助处理器了。如何做这些将在第6节讨论。

3.1CPU缓存概览

在深入CPU缓存的实现的技术细节之前,首先看看在现代计算机系统中,缓存是一个怎样的位置,这对一些读者来说可能有益。
在这里插入图片描述
图3.1展示了一个最小缓存配置。它与早期部署了CPU缓存的系统架构一致。CPU核心不再与主存直接相连。{在更早期的系统中,缓存像CPU和主存一样,与系统总线相连。这更像是一个黑客行为,而不是真正的解决方案。}所有的加载和存储都要经过缓存。CPU内核与缓存之间的连接是特殊且迅速的。简单示意的话,主存和缓存是连向系统总线的,系统总线也被用于系统其它组件的通信。我们将系统总线称为前端总线FSB,这也是现在通用的名字,如2.2节中所述。在本节中我们忽略北桥,因为它是用来达成CPU与主存通信的。

虽然在过去几个十年中,计算机使用的是冯诺依曼架构,经验告诉我们,区分代码所用的缓存和数据所用的缓存是有优势的。Intel从1993年开始做这种区分,并且再也没回头。代码和数据所需的内存区域是非常独立的,这也是为何独立缓存运作得更好。近些年来另一优势浮现:对于大多数的处理器来说,指令译码是很慢的,而缓存指令译码结果可以加速执行过程,尤其在错误断言或无法断言分支而导致的管道空闲的情况下。

在引入缓存不久后,系统变得更复杂了。缓存和主存速度的差别再次加大,因此另一个级别的缓存被引入,它比一级缓存更大,也更慢。因为成本原因,不能仅仅增加一级缓存的大小。如今,在常规用途下甚至也有机器配置了3个等级的缓存。一个有这样配置的处理器的系统如图3.2所示。随着单CPU核心数的增加,缓存等级数在未来甚至还会增加。
在这里插入图片描述
图3.2展示了缓存的3个等级,以及接下来我们会用的命名法。L1d指第一级数据缓存,L1i指第一级指令缓存,等等。注意这只是示意图,实际上从CPU核心到主存的数据流不需要经过任何的高级缓存。CPU设计者在缓存接口设计上有很大的自由度。对程序员来说这些设计选项是不可见的。

另外,我们有多核心的处理器,且每个核心可跑多个“线程”。与一核心,核心上跑1个线程相比,区别在不同的核心拥有对所有的硬件资源的不同的拷贝。(几乎是这样{早期的多核心处理器甚至有不同的2级缓存,没有3级缓存})核心完全是独立运行的,除非它们要同时用同一资源—比如:对外连接。另一方面,线程共享处理器几乎所有的资源。在Intel的实现中,线程仅有几个有限的独立的寄存器,有些也是共享的。现代CPU的完整示意图如3.3所示:
在这里插入图片描述
在上例中,有2个处理器,每个处理器有2个核心,每个核心又有2个线程。线程共享一级缓存。核心(深灰色)各有独立的1级缓存。CPU中的所有核心共享更高级别的缓存。2个处理器(浅灰色的2大块)当然不共享任何缓存。所有这些都很重要,尤其讨论多进程和多线程的应用的缓存效果时。

3.2更高级别上的缓存操作

为理解使用缓存的成本和节约情况,我们得结合机器架构,第2节中的RAM技术,和前文所述的缓存结构的知识。

默认情况下CPU内核读写的数据都存在缓存中。有一些内存区域是不能缓存的,但这仅是OS实现者得关心的事,这对应用程序员来说是不可见的。也有一些指令可以让程序员有意绕过特定的缓存,这将在第6节中讨论。

当CPU需要一个数据字时,首先从缓存中搜索。显然,缓存不能包含整个主存的内容(不然我们也不需要缓存了),但因为所有的内存地址都可缓存,每个缓存条目都被标记了它在主存中的地址。这样要读写一个地址时,可在缓存中搜索标签。这里说到的地址可能是虚拟地址或物理地址,基于缓存实现有不同。

因为标签也需要空间存储,所以如果以字的粒度在缓存中搜索是没效率的。因为在x86机器中对一个32位的字来说,它的tag就有32位甚至更多。此外,由于空间局限性是缓存的基本原则之一,不考虑这一点是糟糕的。因为相邻的内存可能会一起被访问,所以它们也应该一起被加载进缓存。还记得我们在2.2.1节中学到的内容:对于RAM模块来说,能在一行上传输多个数据字而不需要发送新的/CAS信号甚至/RAS信号是更为高效的。所以缓存中的条目不是单个的字,而是由连续的字组成的“行”。在早期缓存中,这些行有32字节,现在的规范是64字节。如果内存总线是64bit,这样一行就要传输8次。DDR支持高效地进行这种传输模式。

当处理器请求内存内容时,一整个缓存行被加载到L1d。每条缓存行的内存地址是通过计算由它的大小掩码计算出的地址值得到的。对于64字节的缓存行来说,这意味着掩码计算出的地址值的低6位归零。被丢弃的这6位用来表示缓存行的offset。其余的位有时用来在缓存中定位该行,和作为标签值。实践中地址值被分为3部分。一个32位的地址看起来如下:
在这里插入图片描述
对于2^ O 字节大小的缓存行来说,低O位被用来当作该行的offset。接下来的S位用来选择“缓存集”。我们很快就会来深入为什么是集而不是单个槽被用于缓存行的更多细节。至于现在知道能存在2^S个缓存集就足够了。剩下的32-S-O=T位用来形成tag。这T位是用来区分别名集{所有地址中有相同S部分的缓存行被认为是同一别名集}中不同缓存行的值。这用来寻址缓存集的S位不必存储在每一个缓存行因为相同缓存集的所有缓存行有相同这个值。

当一条指令修改了内存,处理器必须首先加载一个缓存行,因为指令不会一次修改一整个缓存行(除了一种规则:写合并,在6.1节中会解释说明)。因此在写操作之前整个缓存行要(从内存)加载过来。不能维护部分的缓存行(只能整个地维护)。一个有写操作修改但还未写回主存的缓存行是“脏数据”。当它被写回主存后,脏数据的标识会被清除。

为了能加载新的数据,几乎总是有必要先检查下缓存空间。从L1d驱逐出的缓存行会推入L2(在L2中也会存储同样大小的缓存行)。这意味着L2要有这么大的空间。这可能导致L2向L3推入数据,以及反过来最终推入主存。上述每个驱逐的代价是越来越昂贵的。这里要描述一种现代AMD和VIA处理器更喜欢的独占缓存模型。Intel是这样实现独占缓存{这种概括不完全正确,有一些缓存是独占的,还有一些是有独占的缓存属性。}的:每一个L1d中的缓存行也在L2中存在。因此从L1d中驱逐是更快的(因为已存在于L2,不用为腾空间在L2中也相应做驱逐)。因为L2有足够空间所以浪费空间把一份数据在2处都存储的劣势是很小的,且在驱逐时我们收到了相应的回报。独占缓存一个可能的优势是加载一个新的缓存行只用接触L1d而不用动L2,这是更快的。

CPU可以以各种方式管理缓存,只要为处理器架构所定义的内存模型不变。举例来说,对于一种主动回写脏缓存行到主存的处理器来说,利用低内存总线活动或无活动的时机是相当完美的。在x86和x86-64 的处理器中,有着广泛的缓存架构变体,不同的制造商之间有,同一个制造商的不同模型中甚至也有,证明了内存模型抽象的强大。

在对称多处理器(SMP)的系统中,CPU们的缓存不能各自独立工作。所有的处理器在所有时间应读到相同的内存。这种对统一内存视图的维护被称为“内存一致性“。如果处理器只想看到它自己的缓存和主存,而不见到其它处理器产生的脏缓存行,这种情况下下允许一个处理器直接访问另一个处理器的缓存的代价会特别的高,也会是个巨大的瓶颈。取而代之(的实现是),处理器能探测到别的处理器想要读写缓存行。

如果探测到一个写访问,且当前处理器有一份干净的缓存行拷贝,则它会被标记为不可用。未来有对它的引用需重新加载该缓存行。需要注意的是探测到其它CPU的读访问时不需要使缓存行失效,这样就维护了多份干净的缓存拷贝了。

更成熟的缓存实现允许另一种可能性。如果另一处理器试图读或写的缓存行在第一个处理器上当前被标记为脏数据,则需要另一个措施。在这一情况下,主存中的数据已过期,所以请求的处理器需要从第一个处理器处获取数据。通过窥探(snooping),第一个处理器能注意到这一情况,从而自动发送数据给请求处理器。这一措施绕过了主存,但在某些实现中,内存控制器会注意到这种直接传输,然后将更新后的缓存行存入主存。如果第二个处理器访问后是写操作,第一个处理器接着会将它本地的缓存行失效。

随时间推移,发展出了大量的缓存一致性协议。最重要的是MESI,我们将在3.3.4节中介绍它。它的产出可用几条简单规则总结:
• 一个脏缓存行不能出现在别的处理器缓存上。
• 同一缓存行的干净拷贝可以存储在任意多个处理器缓存上。

如果这些规则能维持,即使在多处理器系统中处理器也能高效使用缓存。所有的处器器需要监控彼此的写请求并在本地缓存中对比地址。在下一节中我们会深入更多细节,关于它的实现,尤其是实现的成本。
最后,我们至少先留一个缓存命中与否的成本的印象。这些是Intel列出的Pentium M的数据:

访问至时钟周期
寄存器<=1
L1d~3
L2~14
主存~240

这些是在真实访问中测量到的CPU时钟周期。有趣的是注意到对于片上L2一大部分(可能甚至是主要部分)的访问时间是由线路延迟造成的。这是一个物理限制,缓存越大情况越差。只有缩小工艺(比如Intel产品线中从Merom的60nm到Penryn的45nm)才能改善这些数据。

表里的数据看起来高,但幸运的是,不是每一次的缓存加载或未命中都要支付一整个代价。时间花费的一部分是可被隐藏的。现今处理器会使用不同长度的内部管道,在管道中解码指令,并为执行作准备。部分的准备是把寄存器要用的数据从内存(或缓存)中加载出来。如果从内存中加载数据发生得足够早,它就可以和其它操作并行,这样整个加载的花费就可能被隐藏了。对于L1d来说通常是有可能的,对一些有长管道的处理器来说,L2也是。

早些开始读缓存存在一些阻碍。它可能简单的是没有足够资源进行内存访问,或者可能最终要加载的地址很迟才能确定,因为它是另一个指令的结果。在这些例子中加载花费不能隐藏(完全不能)。

对于写操作,CPU不必等到值被稳妥地存入内存。只要接下来执行的指令会产生相同的效果,就像值是在内存中,没有什么能阻止CPU走捷径。它可以早执行下条指令。影子寄存器用来存储不再能从常规寄存器访问的数据,在影子寄存器的帮助下,CPU甚至能改变在未完成写操作过程中将要存储的值。
在这里插入图片描述
从图3.4可以看到了缓存行为的效应。后面我们将讨论产生该数据的程序。这是一个简单模拟,模拟以随机方式重复访问一个配置数量的内存数据的程序。每一数据项都是固定大小。元素数量取决于所选的工作集大小。Y轴展示处理一个元素所用的平均CPU时钟周期。注意,Y轴数据的刻度是对数的。这类图中的X轴也应用到这种规则。工作集大小总是以2的幂表示。

该图展示了3个不同的稳定期。这不奇怪:指定的处理器有L1d和L2缓存,但没有L3。凭一些经验我们可以推断L1d是2 ^ 13字节,L2是2^20字节。如果整个工作集大小在L1d大小范围内,对元素每个操作的时钟周期低于10。一旦超过了L1d大小,处理器就得从L2加载数据,平均时间升到28左右。一旦L2不够了,时间跃升到480往上。这时很多,或者大部分的操作得从主存中加载数据。更糟的是:因为数据中有缓存行被修改为脏数据了,又得写回主存。

这张图给足了动力来探究如何提升代码,以帮助提升缓存使用。我们不是在说一些微小的百分数,而是在某些时候可能的指数级的提升。在第6节我们将讨论写出高效代码的技术。接一下来的一节我们会深入CPU缓存设计的更多细节。知道这些知识很好,但是对于本文其余部分不是必须要了解的。所以下面这节可以跳过。

3.3CPU缓存实现细节

缓存实现存在一个问题是,在巨大的主存中每一个单元都潜在地不得不被缓存。如果一个程序的工作集够大,许多主存位置都要竞争缓存中的位置。前面提到缓存大小与内存大小有1:1000的比例并不罕见。

3.3.1关联性

实现一种每个缓存行都能保存任一内存位置的缓存是可能的。这称为完全关联缓存(fully associative cache)。当处理器核心访问一个缓存行时,它要对比每一个缓存行的tag和请求地址的tag。Tag将由地址中除了offset之外的整个部分组成(这意味着,图3.2中的S为0)。

是有缓存是这样实现的,但看下如今L2的数据,将表明这是不能实现的。一个缓存行为64B 的4M缓存将有65536个条目。为达到足够的性能表现,缓存逻辑须能在仅几个时钟周期内从这些条目中找到指定tag的那个。实现这一目标要投入的努力是巨大的。
在这里插入图片描述
比较器需要比较每个缓存行的那个大标签(注意:S为0)。每个连接线旁的字母表示宽度(单位:bit)。如果没有显示则表示为1。每个比较器都要比较2个T位的值。然后,基于比较结果,相应的缓存行内容才能被选中和可用。还需要合并多个含O个数据行的数据集,这些数据集也叫缓存桶。实现这样一个比较器需要大量的晶体管,尤其它还得工作得很快。没有迭代比较器可以做到。唯一能减少比较器数量的办法是反复比较标签。这与迭代比较器不适用的原因相同:耗时太长。

完全关联缓存在小缓存是可行的(如一些Intel处理器的TLB缓存是完全关联缓存),它们真的很小很小,最多几十个条目。

对于L1i,L1d,以及更高级别的缓存,需要另一种方式。我们能做的是限制搜索过程。在最极端的限制下,一个tag精确映射为一个缓存条目。比较很简单:对于4M/64B,65536个条目的缓存,我们可通过第6到21位的共16位地址直接寻址到每一个条目。低6位是缓存行的索引。
在这里插入图片描述
从图3.6可以看到,这种直接映射的缓存很快且实现相对较易。它仅需要1个比较器,1个复用器(图中是2个,因为tag和数据是分开的,但这不是该设计的硬性要求),以及实现选中一个可用缓存行的逻辑。因为速度要求比较器是复杂的但现在只需要1个。这样为提升它的速度可做的努力有很多。在这个方式中复杂点在复用器上。一个简单复用器的晶体管数量以O(log N)的速度增长,N是缓存行的数量。这可以接受但可能也会慢,在某些情况下可以增加晶体管面积来并行一些工作以提速。在增加缓存大小的同时,晶体管的数量也不会增得太多,这一方法很有吸引力。但也存在一个缺点:它只在程序访问的地址均匀分布于直接映射的位数时才奏效。如果不均匀,通常也是这种情况,某些条目是频繁使用的,因此反复被驱逐,而其它的很少使用或留空了。
在这里插入图片描述
这一问题可通过使缓存集相关来解决。缓存集相关的缓存结合了完全关联缓存和直接映射缓存的特点,同时很大程度上避免了它们的弱点。图3.7展示了缓存集相关缓存的设计。标签和数据被分为不同的可被地址选择的集。这与直接映射缓存相似。但每个集里存着少量的有着相同集值的数据,而不是一个。并行比较所有集里的成员的tag,这和完全关联缓存的机制相似。
这种缓存不会被不好彩的,或者对有相同集值成员的有意的地址选择击垮,同时缓存大小不会被可并行工作的比较器数量限制。随缓存增大,仅列的数量要增加(从数字上),而不是行。行数量只会随缓存的关联等级增加。现今处理器的L2或更高等级的缓存的关联等级高达16,L1通常是8。
在这里插入图片描述
一个4M/64B,8路集的关联性的缓存中,有8192个集,仅13位的标签用来寻址缓存集。为了确定一个缓存集中是否有目标地址的缓存行,需要对比8个标签。这能在短时间内完成。我们可从一个实验中知道这很有用。

表3.1展示了一个程序在不同缓存大小,不同缓存行大小,不同相关集大小情况下L2缓存未命中的数据(即gcc,根据Linux内核人员说法,是最重要的基准)。在7.2节中我们将介绍做这种模拟缓存测试的工具。

以防不够明显,这些值之间的关系是:缓存大小=
缓存行大小 * 关联等级 * 集数

缓存地址映射:
O=log2缓存行大小
S=log2集数
3.2节中的数据如下图所示:
在这里插入图片描述
图3.8使表中数据更好理解。它显示的是缓存行大小为32B的数据。从图中可以看到,对于给定大小的缓存,关联等级的确可显著减少缓存未命中的情况。在8M缓存中,从直接映射到2路集关联的缓存,减少了44%的缓存未命中的情况。相比于直接映射缓存,使用集相关缓存的处理器可以保持更大的工作集。

在文献中偶尔会读到,在缓存中引入关联性,可产生与缓存大小加倍相同的效果。在一些极端情况下确实如此,如同我们从图中看到的4M到8M的跃变。但进一步加倍关联性这一效果就真不再如此了。如数据所示,后续的增益变更小。但也不能完全不计。在示例程序中内存使用峰值是5.6M。所以对于8M缓存来说,不太可能多次(超2次)使用到同一缓存集。对于更大的工作集来说,节约效果也更显著,如图中所示,在小缓存中,关联性发挥了更大的效用。

总的来说,对于单线程负载,在超过8后继续增加关联性的收益甚微。引入多核心处理器后,由于它们共享L2,情况有所变化。2个程序使用同一块缓存导致关联性减半(对于4核心处理器,会减到1/4)。所以可以预测,随处理器核心数增加,共享缓存的关联性也要增加。一旦加到顶后(16路集关联已经很难了),处理器设计人员就得使用L3或者更多,同时L2可能只在一个核心子集中共享。

从图3.8中我们还能学到缓存变大对性能的提升效果。这一数据不能在不谈工作集大小的情况下去理解。显然,和主存一样大的缓存好过小一些的,因此对于可测量的增益来说,总体上缓存的大小是无限制的。

如上所述,工作集的峰值是5.6M,它没能给出缓存大小一个最优的最大值,但它允许我们来评估一下数据。问题在于使用的内存不全是连续的,因此,即使我们有16M的缓存,和5.6M的工作集也相冲突(看看16M缓存的2路集关联相对直接映射版本的增益)。但我也可以说32M内存对于相同的这一工作负载的增益微乎其微。可谁又能说工作集会一直不变呢?工作负载会随时间增长,缓存大小也应该要。在买机器时,人们得选择自己愿意花钱的大小的缓存,值得去评估工作集大小。为何这很重要?我们从图3.10的数字中可以看到。
在这里插入图片描述
现在跑了2种测试。在第1个测试中元素被依次处理。实验程序跟随指针n,但数组元素是链在一起的,所以在内存中被按序遍历。这可以从图3.9的下方部分看到。最后一个元素有一个指回第一个的引用。在第2个测试中(图的上方部分)数组中的元素是随机被遍历的。在2个案例中数组元素均形成了一个循环单链表。

3.3.2缓存效应的测量

所有的数据由测量一个可以模拟任意工作集大小,读写访问,顺序或随机访问的程序产生。在图3.4中我们已看到一些结果。程序创建了一个跟工作集大小相关的元素数组:

struct l {
    struct l *n;
    long int pad[NPAD];
  };

所有条目使用n链接在一个循环列表中,要么顺序相连,要么随机相连。即使元素依次排列,从一个条目到另一个条目也是需要指针。pad是负载,且可以增长到任意数值。在某些测试中,数据会被修改,在其它的当中仅会被读取。

我们所指的性能测量是指工作集大小。工作集由struct l数组组成。一个2^ N字节的工作集包含2^N/(struct l大小) 个元素。显然,struct l的大小取决于NPAD大小。对于32位系统,NPAD=7,每个数组元素是32字节。64位系统中是64字节。

单线程顺序访问

最简单的例子是遍历列表里所有的条目。列表中的元素是按顺序排列的,密密麻麻。无论访问是向前或向后都无关紧要,处理器处理2种方向都一样的好。我们测量的是,在按下来的测试中也是,处理一个元素要用多久。时间单位是一个处理器时钟周期。图3.10显示了结果。除非特别说明,所有的测量基于Pentium 4 64位机器,这样structure l 在NPAD=0的情况下是8字节。

在这里插入图片描述
在这里插入图片描述
这头2个测量有被噪声污染。仅因为被测量的负载太低了,不能过滤掉系统其它的部分的影响。我们可以认为所有的值都是4个周期。基于此可以得到3个不同的平台期:
• 工作集在至多2^14字节时
• 从2 ^ 15字节至2^20字节
• 2^21字节及以上
这几阶段可以简单解释为:处理器有一个16KB的L1d和1M的L2。从一个阶段到另一个我们没看见有发生尖锐的改变,是因为系统其它部分也在使用缓存,所以对于程序来说不是缓存不是独占的。具体来说L2是统一性缓存,也用来存放指令(注:Intel使用包容性缓存)。

一个可能不太如我们所料的是不同工作集大小的访问时间。L1d的时间是符合期待的:在P4(Pentium 4)中在命中L1d后的加载时间在约4个周期。但L2的访问呢?在L1d空间不足后我们会期待L2访问每个元素时花掉14个或更多周期。但结果显示仅需要9个。这一不一致现象可被处理器的优先处理逻辑所解释。在使用连续内存的预期中,处理器会预获取下一个缓存行。这意味着当下一个缓存行要被使用时,它已加载一半了。于是用于等待下一缓存行加载的延迟比L2的访问时间少了很多。

预获取的效果在工作集大小上涨至超过L2大小时甚至更为可观。之前我们说主存访问要花费200+周期。仅通过高效的预获取处理器就可能将访问时间降低到9个时钟周期。200和9的区别,这很棒。

我们可以观察到处理器的预获取,至少能间接地观察到。在图3.11中我们看到对于相同工作集大小的不同访问时间,但这次我们再看图中不同大小的structure l。这意味着我们在列表中有更少但更大的元素。不同大小的影响是在列表中元素n间的距离变大了(依然连续)。在图中的4个情况下,距离分别是0,56,120和248字节。

在底部我们可看到前一张图的那条线,在这张图中它或多或少显得很平。仅仅因为其它的情况中访问时间变差了太多。从图中还可以看到3个不同的平台期,以及在工作集较小时误差大(再次忽略吧)。几条曲线在只有L1d参与时大致是重合的。不必做预获取所以在对所有元素的每次访问中都命中L1d。

在命中L2时,我们看到3条新的曲线也比较重合,只是值更高(约28)。这就是L2的访问时间,这说明基本没有从L1d预获取L2。即使NPAD=7时我们需要每次循环时读取新一个缓存行。但NPAD=0时,循环进行8次,才需要下一个缓存行。预获取逻辑不能在每个时钟周期都加载一个新缓存行,所以我们在每次迭代中都看到由于从L2读取的延迟。

在工作集超过L2大小后,情况变得更有趣了。现在所有的4条曲线各不相同。不同的元素大小显然是性能表现差异的一大因素。处理器应能识别步长,在NPAD=15和31时不会去获取不必要的缓存行,因为元素大小小于了预获取窗口(见6.3.1节)。硬件预获取的限制,导致了元素大小阻碍预获取。硬件预获取不能跨页。在每一次NPAD增长中,我们都减少了50%硬件规划器(hardware scheduler)的效能。如果硬件预获取可以跨页,而下一页不存在或无效,则OS得介入来定位页。这意味着程序会经历不由它发起的页错误。这完全是不能接受的,因为处理器不知道页是没显示(not present)还是不存在(not exist)。在后者情况下操作系统要终止进程。这样,在NPAD=7或更高的任何情况下,当我们对每个元素访问时要都加载新缓存行,这时硬件预获取就没什么可帮忙的了。这仅是因为没有时间从内存加载数据,因为处理器忙于读完一个就加载下一个元素。

另一个减速的原因在于TLB缓存的缺失。这是一个存虚拟地址到物理地址转换结果的缓存,第4节中会介绍更多。TLB相当小因为它得极快。如果多个页面被重复访问,多到超过了TLB能存的条目数,从虚拟地址到物理地址的转换就会重复进行。这是这一个相当消耗的操作。随元素大小增加,TLB查找花销分摊在更少数量的元素上。这意味着对列表的每个元素所需计算的TLB条目数更高。

为观察TLB效应我们可以跑另一个测试。在一个测量中我们将元素按顺序排布。设置NPAD=7以使一个元素占一整个缓存行。第二个测量中我们将元素放在不同的页中。每页其余部分不动,也不计入总的工作集大小。{这有一点不连贯,因为在别的测试中我们将结构体中未使用的部分也计入了元素大小,继而我们可以定义NPAD从而使每个元素填满一页。这样工作集大小就可以很不相同。但这不是我们这次测试的要点。且因为无论如何预获取也没有效率了,所以像之前那样做也没有什么改善。}结论是,在第一个测量中,每次迭代都请求一个缓存行,每64个元素,就要有新的一页。第二个测量中,每次迭代都请求一个新缓存行,缓存行在新的页上。
在这里插入图片描述
结果可从图3.12看到。测量在如图3.11中的机器上进行。因为可用的RAM大小限制,工作集要在2 ^ 24字节以内,这使得1G内容要分成几页。在下方的红色曲线与图3.11中NPAD=7的曲线是刚好一致的。我们看见展示了L1d和L2缓存大小的不同阶段。第2条曲线则截然不同了。最显著的一个特点是当工作集大小到达2^13字节开始出现的大尖刺。这就是TLB缓存溢出的时候。一个64字节的元素需要有64个条目的TLB缓存。这里没有出现会影响花销的页错误,因为程序将内存锁住了,因而不会被擦去。

从时钟周期数可以看出计算和存储物理地址至TLB是十分耗时的。图3.12中显示的是一种极端情况,但这也清晰地表明了更大的NPAD导致的减速的一个显著因素,是下降的TLB缓存效能。因为无论从L2还是主存读取缓存行之前,都须计算出物理地址,这样在内存访问耗时之外还增加了地址转换的耗时。这部分解释了当NPAD=31时,每个元素的总访问耗时大于了理论上RAM的访问时间。
在这里插入图片描述
看看修改这些列表中的元素的测试跑出来的数据,我们可以获得更多关于预获取的知识。图3.13展示了3条曲线。这些例子中元素大小都是16B。第一条线是我们熟悉的遍历数组中的元素,这作为基线。第二条线,标记为“Inc”的,仅在遍历中将元素的pad[0]自增1。第三条线标记为“Addnext0”,会取出下一个元素的pad[0]并将它加到当前元素的pad[0]上。

一个简单的推断是“Addnext0”这个测试应该是更慢的,因为它要做的更多。在读取列表的下个元素之前它要先被加载。这就是为何我们会惊讶地看到,在一些工作集大小情况下,这个测试比“Inc”那个跑得更快。因为这种要加载列表中下一个元素的逻辑,基本就是强制预获取。无论何时程序要访问列表中下一个元素,我们都确信它已经在L1d中了。作为结果我们看到,在工作集大小不超过L2的情况下,“Addnext0”测试表现得和“Follow“测试一样好。

然而“Addnext0”测试比“Inc”测试更快用尽L2。它的确需要从主存中加载更多数据。这就是为何在工作集在2^21字节时,它的时钟周期达到了28个。这是”Follow”测试14个时钟周期的2倍。这也很好解释。因为在另外2个有修改内存的测试中,为了在L2中为新缓存行腾空间而驱逐的缓存数据不能被丢弃,相反得写入内存。这意味着前端总线的可用带宽会减半,继而使数据从内存传输至L2的时间加倍了。
在这里插入图片描述
关于按顺序高效地使用缓存的方式的最后一个方面是缓存大小。这显而易见,但仍值得一提。图3.14展示了基于128字节的元素(NPAD=15,64位机器),自增测试的数据。这次的数据是基于不同机器。头2个是P4(Pentium 4),最后一个是Core 2处理器。头2个的缓存大小不同。第1台机器有32k L1d和1M L2,第2台有16k L1d ,512k L2和2M L3。Core 2处理器有32k L1d 和4M L2。

这个图有趣的部分不一定在于Core 2处理器相比之下表现得有多优秀(虽然确实让人印象深刻)。此处的要点是当工作集过大,大到相应的最后一级缓存和主存都深度参与进来之后的部分。
在这里插入图片描述
正如预期那样,最后一级缓存越大,曲线停在与L2访问时间相关的低水平状态就越长。重要的部分是它所提供的性能优势。第2个处理器(有一点老了)在2^20字节工作集上表现得比第1个快2倍。这都是因为最后一级缓存大小的增加。有着4M L2的Core 2处理器表现得甚至更好。

对于随机访问来说,这些可能没有如此大的影响。但如果负载能改造到适应于最后一级缓存的大小,程序的性能会得到相当大的提升。这也是为何值得花费额外的钱去买更大缓存的处理器。

单线程随机访问的测量

我们看到处理器能通过预获取缓存行到L2和L1d,而隐藏大部分访问主存甚至L2 的延迟。但这只在内存访问是可预测的情况才可行。在这里插入图片描述
如果访问不可预测,或是随机发生的话,情况就相当不同了。图3.15比较了列表中每个元素的顺序访问时间(与图3.10一样),和元素在工作集中随机分布时访问的时间,它的顺序是由链表决定的,所以是随机的。处理器无法可靠地预获取数据。这种情况下唯一可做到的预获取是,短时间内先后被访问到的数据恰好是在内存中相临近的。

在图3.15中有2点需要注意。第一,当工作集增大后,需要的访问时钟周期的增大。机器上访问主存的时间是200-300个时钟周期,但是图中是450往上。我们之前也遇见过这种情况(对比图3.11)。这时自动预获取实际造成的是劣势。

第二个有趣的点是曲线不是像顺序访问情况下的,有不同的平台期。曲线是在持续上升。为解释这点我们可以测量在不同的工作集大小状况下该程序对L2的访问,测量结果见图3.16和表3.2。

图显示当工作集大于L2的大小后,缓存的未命中比率(L2未命数/L2访问数)开始增长。曲线与图3.15中的有相似的形状:它快速上升,再微微下降,接着又上升了。它与列表中每个元素的访问时间图有着很强的关联性。L2的未命中比率将一直增长直至接近100%。如果有足够大的工作集(和RAM),那么随机选取的任意缓存行都在L2中或者正在被加载的概率可任意降低。

持续增长的缓存未命中率可以部分解释一些成本。但还有其它的因素。从表3.2的L2/#Iter行我们可以看到,在程序的每次迭代中,L2的总使用次数是在上升的。每行的工作集都是上一行的2倍。所以,在没有缓存的情况下我们会料想访问主存的次数翻倍。但是由于缓存的缘故,和(几乎)完美的预测,在顺序访问过程中L2的使用只上升了一点儿。这一增长只是因为工作集大小的增长,没有其它原因。
在这里插入图片描述
在这里插入图片描述
在随机访问过程中,当工作集大小加倍,每个元素的访问时间增长了超100%。这说明,在工作集仅仅是翻倍的情况下,列表中元素的平均访问时间就增长了。这背后的原因是TLB的未命中率上升了。在图3.17中我们可以看到,NAPD=7的情况下,随机访问的花费。只是这次测量中随机访问是经过修改的。在普通情况下,整个列表的随机分布是在一个块中(由标签∞表示),其它的11条曲线是在更小的块中的随机分布。标签是“60”的曲线表示在60页(245.760 字节)内随机分布。这意味着所有在一个块中的列表元素会先遍历完,再去到下一个块。这会造成在任意时间使用的TLB条目数是在有限范围内的。

NPAD=7时元素大小是64字节,这与缓存行大小一致。由于列表元素的随机分布,硬件预获取不会有什么效果,肯定不会超过很少的几个元素。这意味着L2的未命中率不会由于一个块中的元素随机分布的不同而有显著差异。随着块大小增加,测试结果的表现趋近于单块随机分布的曲线。这意味着后面的这一测试表现深受TLB未命中率的影响。如果TLB未命中率能减少,性能将上升非常多(在后面的测试中我们可以看到会上升至38%)。

3.3.3写行为

在我们开始探究,当多个执行上下文(线程或进程)在使用同一个内存时缓存的行为这前,我们需要先看看缓存实现的细节。缓存应有一致性且一致性对于使用者层级的代码来说应是透明的。内核的代码则不同,它需要偶尔地清空缓存。

确切来说,这意味着如果一个缓存行被修改了,这之后对于系统来说就要像没有缓存 一样,而且主存中相应地址的数据已经被修改过了。这可以通过2种方式或策略实现:
• 直接(write-through)缓存实现
• 回写(write-back)缓存实现

Write-through是最简单的缓存一致性实现。如果在一个缓存行上发生了写,处理器马 上也将它写入主存。这保证了在所有时间里,主存和缓存是同步的。缓存内容可以简单在缓存行被替换后丢弃。这一策略很简单但不是很快。举例来说,一个程序修改多次修改同一变量时,会在FSB上造成大量传输,即使这个数据可能不会在任何其它地方用到,且可能生命周期也很短。

Write-back则更加成熟。这时处理器不会在缓存行发生改变后即刻将它回写至主存。取而代之的是,缓存行被标记为脏数据。当在未来这一缓存行要被丢弃时,会指示处理器把脏数据回写,而不仅仅是丢掉内容。

Write-back缓存可能表现得好非常多,这是大部分有着良好处理器的系统都用这种方式的缓存的原因。处理器甚至可以利用FSB的空闲空间将缓存行在撤离之前存入主存。这使得脏数据标记被清空,这样当需要缓存空间时,处理器可以直接丢弃该缓存行。

但write-back实现也有一个很大的问题。当多个处理器(或核心,或超线程)可以访问同一个内存时,需要确保在所有时间里各个处理器都看到相同的内容。如果一个缓存行在处理器上是脏数据(它还没有被回写)时,另一个处理器试图读取它,读操作就不能仅仅从主存去读。取而代之的是应读取第一个处理器的缓存行内容。在下一节我们会来看看当前这是怎样实现的。

在这之前还有2 种缓存策略:
• 写合并(write-combining)
• 不可缓存(uncacheable)

这2种策略是在特殊的地址空间使用的,这些地址没有对应的真实RAM内存。内核对于一些地址范围(在x86处理器上是内存类型范围寄存器,MTRR们)使用这些策略,其余操作自动完成。MTRR们还可以使用write-through和write-back策略。

写合并是一种有限的缓存优化,更常用于设备上的RAM如显卡。因为到设备的传输成本比到本地RAM的更高,避免发生过多的传输甚至更为重要。仅因为缓存行上的一字发生改变就传输整个缓存行是很浪费的行为,如果下个操作是修改下一个字呢。我们可以想象这是很常见的情况,屏幕上水平相邻的像素块,在大部分情况下在内存中也是相邻的。如名,写合并就是把多个写访问合并,然后再回写缓存行。在理想情况下,整个缓存行是一个字接一个字被修改的,仅仅当最后一个字被写入后,整个缓存行才被写入设备。这可以极大地加速对设备RAM的访问。

最后是不可缓存的内存。这意味着内存地址后面没有对应的RAM。它可能是一个特殊的被硬编码的地址,在CPU之外有一些功能。在一些商业硬件上,很有可能是映射到接入总线(PCIe等)的卡或设备的一个内存地址范围。在嵌入式板卡中有时可以看到用来开关LED灯的地址。缓存这一类地址显然不好。在这种背景下LED是用来调试,或者报告状态的,人们需要尽快能看到。PCIe上的卡片内存可以在CPU不干预的情况下改变,所以它的内存也不应该被缓存。

3.3.4多处理器支持

在前面的内容中我们指出了当多个处理器参与内存访问时,会发生的问题。即使是多个核心的处理器在不共享的缓存(至少有L1d)上也会这一问题,

从一个处理器直接访问另一个的缓存是完全不实际的。首先,仅仅是连接不够快。可替代的另一种方案是在需要时将缓存传输到另一处理器。注意这一方法也适用于同一个处理器中不共享的缓存。

现在的问题是什么时候得传输缓存行?这也很容易回答:当一个处理器需要读或写在另一个处理器上标记为脏的缓存行。但处理器如何判断缓存行是否在另一个处理器被标记为脏数据?我们假设被其它处理器加载了的缓存行是次优(最多算是)的。通常大部分的内存访问都是读,结果造成的缓存行都不是脏的。处理器对缓存行的操作是频繁的(当然,不然为什么写这篇文章),这意味着在每次改变了缓存行后广播出这一消息是不可行的。

这些年来发展出了MESI缓存一致性协议(修改,独占,共享,失效)。这一协议是由满足这一协议的缓存行被使用后会有的4个状态命名的:
• 修改:本地处理器修改了缓存行。这也表示当前的是独一份的缓存
• 独占:缓存行没有被修改,但是不能被加载进别的处理器缓存中
• 共享:缓存行没有被修改,可能存在于别的处理器缓存中
• 失效:缓存行失效了,比如:未被使用

协议是这些年来经过发展到这一步的,从更为简单,不复杂,但也更低效的版本开始。有了这4个状态我们就可高效地实现write-back缓存,同时也支持不同处理器对只读数据的并发使用。
在这里插入图片描述
状态改变的实现是通过处理器的监听、侦听其它处理器实现的,没有太多的工作。处理器的特定操作会由向外的pin而得以宣告,因此使当前处理器的缓存处理对外界来说是可见的。问题中的缓存行地址在地址总线上是可见的。在接下来对这些状态和它们的转换的描述中(图3.18所示),我们将会指出何时总线会参与进来。

在最初所有的缓存行是空的,所以也是无效状态。当数据被加载进缓存来写,它的状态变更为已修改。如果数据被加载来读,则新状态取决于该缓存行是否已被其它处理器加载。如果已被其它处理器加载,则新状态为共享,否则是独占。

如果一个已修改状态的缓存行被本地处理器读或写,指令可以使用当前缓存内容,且状态不变更。如果第二个处理器想读该缓存行,第一个处理器需要发送它的本地数据到第二个处理器上,并将缓存状态变更为共享状态。被发送到第二个处理器的数据也会被内存处理器接收和处理,将数据存储至内存中。如果没有做完这一步,缓存行不能被标记为共享状态。如果第二个处理器想修改该缓存行,第一个处理器会发送本地缓存行,且标记它为无效。这就是臭名昭著的“所有权请求”(RFO)操作。在最后一级缓存中执行此操作,就像I(Invalid)->M(Modified)转换一样,成本比较高。对于write-through缓存我们还需要加上将新缓存行写入下一个高级缓存或主存的时间,进一步地增加了时间成本。

如果缓存行是共享状态,本地处理器想读取它,则不需要状态改变,读取请求可以通过缓存完成。如果本地写缓存行,缓存行也可直接使用,但状态会改为已修改。这也需要所有在其它处理器上的缓存行副本都被标记为失效。因此写操作得通过一条RFO消息通知其它处理器。如果该缓存行被第二个处理器请求来读,则不须别的操作—主存包含着当前的数据,且本地的缓存行状态是共享。在第二个处理器想写(RFO)时,本地简单地将其标记为无效,不需要总线操作了。

将独占状态与共享状态区分开来最关键的不同点是:本地的写操作不须在总线上通知。本地的缓存副本是唯一的。这可能是一个巨大的优势,所以处理器会试着保持尽可能多的独占状态而不是共享状态。后者是一个回退策略,以防在当时当刻不可发送信息。独占状态也可以被省略而不造成一点儿功能问题。(省略它)只是性能会有影响,因为E(Exclusive)->M(Modified)转换比S(Shared)->M(Modified)转换要快非常多。

从对状态转换的描述中,我们可以清晰地看到多处理器操作的特有花费在哪。是的,填充缓存还是代价高昂,但现在我们不得不留意RFO消息。任何需要发送这一消息的时候操作都会变慢。

有2种情况必须发送RFO消息:
• 线程迁移到另一个处理器,所有的缓存行都要一次性地移动到新处理器上
• 一个缓存行真的被2个不同处理器所需要{在更小的级别上,对于同一个处理器的2 个核心也是如此,花费只是更小一些。 很可能要多次发送RFO消息。}

在多线程或多进程的程序中总有一些同步的需求。同步是通过内存实现的。因此有一些有效的RFO消息。 发送RFO还是得保持尽可能的低频。还有一些别的要发RFO消息的情况。我们将在第6节中探讨这些场景。缓存一致性信息必须在系统的处理器间传布。只有当系统中所有的处理器都可以回复消息时,一次MESI转换才可能完成。这意味着一次回复可能花费的最长时间决定了一致性协议的速度。{举例来说,这就是为什么我们看到如今的AMD Opteron 系统有三插槽。由于处理器只有3个超链接,其中一个要用于连接南桥,所以每个处理器正好相距一跳。}总线上发生冲突是有可能的,在NUMA系统中延迟可能很高,当然,仅仅是流量也会减慢速度。所有这些都是避免不必要流量的充分理由。

当存在多个处理器时,还会引发一个问题。这一影响和不同机器高度相关,但原理上问题始终存在:FSB是共享的资源。在大多数机器中所有处理器通过单一总线与内存处理器相连(见图2.1)。如果一个处理器就能占满总线(通常来说也是),那么2个或4个处理器共享总线将会更大地限制到每个处理器能用的带宽。

即使每个处理器都能有自己的连接内存处理器的总线,如图2.2所示,仍存在连接至内存模块的总线,通常来说这条总线只有一条。即使在如图2.2的外部模块中,对同一内存模块的并发访问也会限制带宽。

这在每个处理器都有自己的本地内存的AMD模型中也是如此。确实,所有处理器可以并发且快速地访问它本地的内存。但对于多线程或多进程的程序来说,至少时不时地就得访问相同的内存区域以实现同步。

由于必须的同步所需要的带宽有限,严重限制了并发。程序需要小心地设计来最小化从不同的处理器或核心访问相同的内存地址。接下来的测量将展示这一点,以及其它与多线程代码相关的缓存效应。

多线程测量

为了理解在不同处理器上并发访问相同缓存行带来的严重问题,我们在这里看看之前使用的相同程序的一些性能表现图。但这一次,同一时间有超过一个线程在运行。测量的是这些线程中最快的一个的数据。这意味着当所有线程跑完所花费的时间甚至是更多的。使用的机器有4个处理器,线程用到了4个线程。所有的处理器通过一条总线连至内存处理器,且仅有一条总线连至内存模块。
在这里插入图片描述
图3.19显示的是对128字节的条目(NPAD=15,在64位机器上)的顺序只读访问的性能。我们可以预期单线程的曲线与图3.11中的相似。因为测量是在不同机器上所以数值有区别。

这张图中最重要的部分当然是在跑多线程时的行为。需要注意当遍历链表时,没有修改内存,也不需同步各线程。即使在没有RFO消息要发,所有的缓存行是共享状态的情况下,我们可以看到当使用2个线程时,最快的那个有达18%的性能下降,在4个线程时,达34%。因为不需要在不同处理器间传输缓存行,这个减速仅仅是因为以下2个瓶颈中的1个或者都有:处理器连接内存控制器的共享总线,和内存控制器连接内存模块的总线。当工作集大小超过了本机的L3大小,所有的3个线程都需要预获取新的列表元素。即使是2线程,带宽也不足以支持线性扩展(即运行多个线程不会产生任何影响)。

当修改时,情况更糟了。图3.20展示了顺序自增测试的结果。
在这里插入图片描述
该图的Y轴采用了对数刻度。不要被“明显”的微小差异给骗了。在跑2个线程时还是有18%的性能损耗,但现在跑4线程的损耗达到惊人的93%。这意味着当使用4线程时,预获取和回写流量一起将总线挤满了。

我们使用对数刻度来展示工作集在L1d范围的结果。可见到的是,只要跑超过1个线程,L1d基本上就不够用了。当L1d放不下工作集时,单线程的访问时间才超过了20个时钟周期。当跑多线程时,访问时间马上超过20个时钟周期,即使是在最小工作集时。

问题有一个方面没有展示出来,从这个测试程序中很难测出来。即使测试中修改了内存,因此我们期待一定有RFO消息发送,但在L2范围期间使用超过1个线程时,我们并没有看到更高的花销。程序不得不使用大量的内存,且所有的线程必须并发访问相同内存。在没有大量的同步操作下是很难实现的,而同步占了执行时间的大头。
在这里插入图片描述
最后我们在图3.21中得到了“Addnextlast”随机访问内存测试的数据。这些数据是为了展现这些令人吃惊的数据。现在在极端情况下,处理一个列表元素要花费1500个时钟周期。使用更的线程会带来更多的问题。我们可以总结多线程的效能如下表:
在这里插入图片描述
表中展示了图3.21中最大工作集情况下,多线程跑的3组效能数据。数据是测试程序在最大工作集情况下,用2或4线程跑的最快可能速度。对2个线程来说,加速的理论极限是2,对4线程来说是4。对2线程来说速度也没那么差。但对于4线程来说,最后一个测试的数据显示,在超2线程后再扩展线程数几乎不值当。额外的收益很微小。如果我们将图3.21中的数据有一点差别地展示出来,我们可以更容易看到这一点。

在这里插入图片描述
图3.22中的曲线展示的是加速因子,即相对于单线程执行代码来说的相对表现。我们得忽略最小的工作集数据 ,测量结果不够精确。工作集在L2和L3大小范围时,我们可以看到确实获得了线性的增速。几乎分别是2倍和4倍。但一旦L3不能放下工作集时,数据就崩了。数据塌到2线程和4线程的加速结果是相同的(从表3.3的第4列可以看到)。这是为何人们很难找到主板上有超过4CPU插槽的原因之一,这些CPU都使用相同的内存控制器。有更多处理器的机器需要不同方式地构造(见第5节)。

这些数据不通用。在一些情况下即使最后一级缓存大小能放下工作集,加速效果也不是完全线性的。事实上,有一个常态是线程们不总是如测试程序中是解耦的。另一方面一个大工作集仍有可能从超过2线程中获益。但要做到这点需要一些思考。我们将在第6节谈论一些可行的方案。

特殊案例:超线程

超线程(有时也被称为对称多线程,SMT)是通过CPU实现的,因为线程们不能真的并发运行,所以这是一个特殊案例。超线程共享几乎所有的资源,除了寄存器集。独立的核心和CPU仍可以并行运行,但每个核上实现的超线程却受到这个限制。理论上一个核心上可以有很多线程,但是至今为止,Intel的CPU上每个核心最多有2个线程。CPU负责为线程复用时间。单凭这点不能实现太多。真正的优势在于当当前超线程延时时,CPU可以为另一个超线程规划。在大多数情况下,延时是因为内存访问。

只有当2个线程的组合运行时间比单线程运行时间更短时,程序上在超线程核心上跑2个线程才更高效。这是因为可能重叠不同的内存访问的等待时间,内存访问通常是顺序发生的。一个简单计算可以展示为达到一定的加速效果,缓存命中率的最小需求。

程序的执行时间可以被约化为一个只有一级缓存的简单模型,如下所示(见[htimpact]):
Texe = N[(1-Fmem)Tproc + Fmem(GhitTcache + (1-Ghit)Tmiss)]

各变量的含义如下:
N=指令条数
Fmem=N条指令中需要访问内存的条数比例
Ghit=加载中命中缓存的次数比例
Tproc=执行每条指令的时钟周期
Tcache=缓存命中时的时钟周期
Tmiss=缓存未命中时的时钟周期
Texe=程序的执行时间

为了使用2线程有意义,需要2个线程中每个的执行时间最多只能是单线程执行的时间的一半。双方唯一不同的变量是缓存命中数。如果我们算出方程中所需的最小缓存命中数,不要将线程的执行时间减速至50%或更多,就能得到图3.23中的数据。
在这里插入图片描述
图中X轴代表单线程代码中的缓存命中率Ghit 。Y轴展示了多线程代码所需要的缓存命中率。这个值总是不能比单线程的缓存命中率更高,因为不然的话,单线程也能用上这个改良代码。对于单线程命中率低于55%的程序来说,总是可以通过多线程运行受益。因为CPU因为缓存未命中而或多或少足够空闲,去跑第2个超线程。

绿色区域就是目标区域。如果对线程的减速少于50%,每条线程上的工作负载减半,那么组合运行时间将可能少于单线程运行时间。对于这里使用的建模系统(有着超线程的P4),一个在单线程情况下有60%命中率的程序,需要在双线程情况下至少有10%的命中率。这通常是可行的。但如果单线程代码有着95%的命中率,则多线程代码需要至少80%。这更难了。这一问题对于超线程来说尤其如此,因为现在每个超线程可用的有效的缓存大小(这里是L1d,实践中也有L2等)被减半了。每一个超线程在用同样的缓存来加载数据。如果2个线程使用的工作集没有重叠,则原有的95%的命中率也会减半,因此大大低于需要的80%。

超线程因此只在有限的情况下有用。单线程代码下缓存命中率必须足够低,以至于在缓存大小减半情况下,新的命中率仍能满足上面的公式。当且仅当这时使用超线程才有意义。在实践中结果能不能更快,取决于处理器能否有效地重叠一个线程的等待时间来执行另一个线程。并行化代码带来的压力必须加入新的总运行时间计算,而这一额外花销往往不可忽略。

在6.3.4节中我们会看到一种技术,它将使线程紧密合作在一起,并且这种在通用缓存中紧密的耦合实际上是一个优势。只要程序员愿意花时间精力来扩展他们的代码,这一技术可以应用到很多情况中。

我们可以清晰知道的是,如果2个超线程在执行完全不同的代码(即2个线程在OS中被当作不同处理器使用,来处理不同的进程)缓存大小的确会被减半,这使得缓存未命中数显著上升。OS的这一规划实践是很有问题的,除非缓存足够大。除非机器的工作负载,由通过其设计确实可以从超线程中获益的进程所组成,否则最好是在计算机的BIOS中关掉超线程。{另一个使用超线程的原因是调试,SMT另人惊奇地擅长找出并行代码中的系列问题。}

3.3.5其它细节

到现在为止我们讨论了由3个部分组成的地址:标签,集索引,缓存行偏移量。但实际上用到的是什么地址呢?现今所有相关处理器提供了用于处理的虚拟地址空间,这就是说有2种类型的地址:虚拟的和物理的。

虚拟地址的问题是它不唯一。同一个虚拟地址,随时间变化,会指向不同的内存地址。在不同处理器上的同一地址也可能指向不同的物理地址。所以总是最好使用物理地址,是吗?

这里的问题是指令使用的虚拟地址需要在内存管理单元(MMU)的帮助下被转换为物理地址。这不是一个微小操作。在执行一个指令的管道中,物理地址可能是在晚一些的阶段才可用。这就是说缓存逻辑要非常快地判断某处内存是否被缓存了。如果可以用上虚拟地址,查询就能在管道中更早进行,而在缓存命中的情况下,内存内容就可用了。结果就是更多的内存访问花销可以在管道中被隐藏了。

当前处理器设计者在用的虚拟地址是对第一级缓存的标签。这些缓存很小,被清空也无妨。至少在进程的页表树改变时,部分清空缓存是必须的。如果处理器有一个指令用到的虚拟地址范围变化了,这就有可能避免了一整个的清空。由于L1i和L1d的低延迟(约3个时钟周期),使用虚拟地址几乎是强制的。

对于包含了L2,L3…的更大的缓存,需要对物理地址的标签化。这些缓存有更高的延迟,从虚拟->物理地址的转换可以及时完成。因为这些缓存更大(即被它们被清空时大量的信息丢失),且重新填充花费的时间很长,因为主存访问的延迟,所以清空它们通常更有代价。

总的来说,应该不必了解在那些缓存中如何处理地址的细节。这些不能被改变,且可能影响性能相关的所有因素通常是一些需要被避免的,或者需要付出高昂代价的。缓存容量溢出是糟糕的,如果被使用的缓存行中大部分都在同一个集合中,所有的缓存都会早早遇到问题。后者可以通过使用虚拟寻址的缓存规避,但是在使用物理寻址的缓存上,从用户级别的进程上避免该问题是不可能的。人们得记住的唯一细节是不要在同一进程中将同一物理内存位置映射到2个或多个虚拟地址,如果可能的话。

另一个对于程序员来说不怎么感兴趣的缓存细节是缓存替换策略。大多部缓存会首先驱逐最近最少使用(LRU)的元素。这总是一个好的默认策略。随着关联性的提升(并且 因为增加了更多的内核,在接下来几年里关联性可能确实会进一步提升),维护LRU列表的成本变得越来越高,我们可能会见到采用不同的策略。

在缓存替换方面,程序员能做的不多。如果缓存使用了物理寻址,就无法找到缓存集如何与虚拟地址关联了。有可能所有逻辑页中的缓存行被映射到相同的缓存集中,留下很多未被使用的缓存。如果发生了,也是OS去安排让这类情况不要频繁发生。

随着虚拟化的出现 ,事情变得更加复杂。现在即使是OS也无法控制对物理内存的分配。虚拟机监视器(VMM,也叫hypervisor)负责对物理内存的分配。

程序员能做的最好的是a)完全使用逻辑内存页,以及b)使用尽可能大的页,以尽可能地分散物理地址。更大的页还有别的益处,但这是另一个话题了。(见第4节)

3.4指令缓存

不只是处理器所用的数据会被缓存。被处理器所执行的指令也会被缓存。然而,这种缓存比数据缓存存在更少问题。这有几个原因:
• 被执行的代码量取决于需要的代码量。代码量通常来说取决于要解决的问题的复杂度。问题的复杂度是确定的。
• 程序中数据的处理是由程序员设计的,但程序的指令是由编译器生成的。编译器作者知道生成好代码的规则。
• 程序流比数据处理模式更容易预测。当今的CPU非常擅于探测模式。这对预获取数据有帮助。
• 代码总是有很好的空间和时间局限性。

有一些规则是程序员需要遵守的,它们主要由如何使用工具的规则组成。我们将在第6节中讨论它们。在这里我们仅谈论指令缓存相关的技术细节。

自从CPU核心的时钟速度快速增加后,缓存(即使是第一级缓存)与核心间的速度差别越来越大。CPU管道化了。这就是说指令的执行是按不同阶段发生的。首先是将指令解码,然后是准备参数,最后是执行。这一管道可能相当长(对于Intel的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的程序。所有由常规工具链构建的程序代码都是受写保护的。程序员得发挥强大魔法来在链接时创建出代码页可写的可执行文件。这一切发生时,现代的Inter x86和x86-64处理器有专门的性能计数器,会记录自修改代码的使用情况。在这些计数器的帮助下,即使程序因权限放宽能执行成功,也能很容易识别出有SMC的程序。

3.5缓存未命中的因素

我们已经看到,当内存访问没有命中缓存时,时间花销像火箭一样上升。有时这情况不可避免,所以了解具体的花销,可以做点什么来缓解这一问题,是重要的。

3.5.1缓存和内存带宽

为了更好理解处理器的能力我们测量了最佳情况下的带宽。这一测量结果相当有趣因为不同处理器版本表现有很大的不同。这是本节充满了不同的几台机器的数据的原因。被测量的程序使用了x86和x86-64处理器的SSE指令集,来一次性加载或存储16字节。工作集由1kB到512M,就和我们其它的测试一样,被测量的是每个时钟周期能加载或存储多少字节。
在这里插入图片描述
图3.24展示的是64位Intel Netburst处理器的性能表现。当工作集在L1d大小范围内处理器能在每个周期读满16字节,即每周期完成1次指令加载(movaps 指令一次移动16字节)。测试中没有对读出来的数据做任何处理,我们测量的只是读指令本身。一旦L1d大小不够了,性能剧烈下降到少于6字节每周期。在2^18字节处的下降是因为DTLB缓存的耗尽,这意味着对于每一个新页面都需要额外的工作来加载。因为读是顺序的,所以可以完美预测访问,并且对于任意大小的工作集,FSB都可以以5.3字节每周期速度传输内存内容。但预获取数据没有冒泡至L1d。当然这些数字在真实程序中是不可能达到的。把它们当作是实践的极限。

比读性能更令人惊讶的是写和复制性能。写性能,即使在小的工作集情况下,也从没有上升至超过4字节每周期。这说明,在这些Netburst处理器中,Intel选择了在L1d使用write-through模式,性能显然被L2限制了。这也解释了在将一块内存区域复制到另一块不重合的区域上的复制测试中,相比写性能没有差得太多。必要的读操作快得如此多,可以和写操作部分重合。最值得记下的写和复制测量结果细节是,在L2不够后很差的性能表现。性能降到了0.5字节每周期!写操作比读操作慢了10倍。这也就是说优化那些操作对于程序性能来说甚至更为重要。

在图3.25中我们看到是在同一个处理器上但是2个线程运行的结果,每个线程各固定在处理器的两个超线程中的一个。
在这里插入图片描述
图展示了与前一个测试规格相同的测试下性能表现的不同,因为测试并行线程的缘故曲线有一点抖动。结果如预期。因为超线程共享所有的资源,除了寄存器。每个线程只有一半的缓存和可用带宽。这意味着即使每个线程都要等待很久,能给到另一个线程执行时间,但也没有用因为另一个线程也要等着访问内存。这真实地展示了使用超线程会更糟的可能性。
在这里插入图片描述
相比于图3.24和3.25,图3.26中的Intel Core 2处理器的结果看起来相当不同。这是一个双核处理器,配有4倍于P4上的L2大小的共享L2。但这就解释了写和复制性能下降的推迟。

这还有更大的不同。读性能在不同工作集范围内全程都徘徊在最佳的16字节每周期。在2^20字节后的下降仍是因为工作集对于DTLB来说太大了。有这么高的表现,不仅意味着处理器能够及时地预获取和传输数据,也意味着数据被预获取进了L1d。

写和复制表现也有巨大不同。处理器没有采用write-through策略。写数据被存储在L1d,只在必要时被驱逐。这使得写速度近于最佳的16字节每周期。当L1d不够后,性能下降得很快。对于Netburst处理器,下降得更多。由于读性能的高表现,差距在此处更大了。事实上,当L2也不够后,速度差异来到了20倍!这不是说Core 2处理器性能差。相反的,它们的性能总是优于Netburst的内核。

在这里插入图片描述
在图3.27中,测试里有2个线程在运行,每个都在Core 2的双核心中的一个上。每个线程都访问相同的内存,但不是完全同时地。读性能和单线程情况下没有不同。可以看到更多的抖动,在这多线程测试案例中是可预料的。

有趣的点在工作集大小在L1d范围内时写与复制的性能。如图中所示,数据表现得像不得从主存读取一样。双方都在竞争同一个内存位置,并且发送缓存行的RFO消息。问题的点在于即使2个核心共享L2缓存,这些请求也不是以L2缓存的速度处理的。一旦L1d不够了,被修改的条目被每个核心的L1d清除,加载进共享的L2。这时性能上升了非常多,因为现在L1d的未命中被L2补充了,L1d中的数据只有在还没有被清除时才需要发送RFO消息。这是我们看到在这些工作集大小时速度下降50%的原因。渐近行为符合预期:因为2个核心共享FSB,所以每个核心得到一半的FSB带宽,这意味着在大型工作集情况下,每个线程的性能是单线程情况下的表现的一半。

因为这些是同一个商家的不同版本处理器的区别,所以当然也值得看别的商家的处理器的性能表现。图3.28展示了AMD家庭的Opteron处理器的性能。这种处理器有64kB L1d,512kB L2,和2M L3。处理器的所有核心共享L3缓存。性能测试的结果如图3.28。

在这里插入图片描述
可以注意到的第一个细节是如果L1d缓存够用,这种处理器每周期可以处理2条指令。读性能超过了32字节每周期,写性能甚至也是18.7字节每周期这么高。但读曲线变平得很快,平后是2.3字节每周期,很低。测试中的这种处理器不会预获取任何数据,至少不足够。

另一方面写曲线随不同的缓存大小而表现不同。最佳的表现在L1d满载时实现,然后就降到L2的6字节每周期,再降到L3的2.8字节每周期,最后是当L3也不能装下所有数据时的0.5字节每周期。L1d的表现超过了Core 2处理器,L2的访问是相同的快(因为Core 2有更大的缓存),L3和主存的访问是更慢的。

复制的性能不能比读或写更好。这是我们看到曲线一开始由读占据优势,再由写占据优势的原因。

Opteron处理器的多线程表现如图3.29所示。
在这里插入图片描述
读性能基本不受影响。每个线程的L1d和L2工作得如之前,这个例子中L3也没有很好地预获取数据。这2个线程不会对L3造成过大的压力。测试中大的问题是写性能。线程共享的所有数据都要经过L3,这种共享好像非常没效率,即使L3能放下整个工作集,访问开销也比仅L3访问要高很多。把这个图与3.27对比我们可以看到Core 2处理器中2个线程在合适的工作集范围中以共享L2的速度运行。这种级别的表现在Opteron处理器中仅在很小的工作集范围内实现过,而且在这时的速度也只是L3的访问速度,慢于Core 2的L2。

3.5.2关键字加载

内存从主存中以块的形式加载进缓存,块的大小比缓存行要小。当前是每次传输64位,而缓存行的大小是64或128字节。这就是说每个缓存行要传输8或16次才能完成。

DRAM芯片可以在burst模式下传输64位的块。这能做到在没有内存控制器的进一步命令下填充好缓存行,因而也就没有相关的可能发生的延迟。如果处理器预获取缓存行的话,这可能是最好的操作方式。

如果程序对数据或指令的缓存访问未命中(因为第一次用到数据而造成的必然结果,或是因为缓存空间有限而需要驱逐缓存行)情况就不同了。程序请求的缓存行中的字可能不是缓存行中第一个。甚至在burst模式,双倍数据传输速率下,独个的64位块是在不同的时间接收到的。每一个块会比前一个慢4个CPU时钟周期,或者更多。如果程序请求的字在缓存行中排第8,程序就得在第一个字到达后,额外等待的30个时钟周期,或以上。

事情不是非得要这样发生。内存控制器可以以不同的顺序请求缓存行中的字。处理器可以告诉内存控制器程序在等的是哪个字,即关键字,这样内存控制器就可以先请求它。这样一接收到那个字程序就可以继续了,在同时间内接收到的缓存行中剩下的字还不是连续的。这一技术就是关键字优先和早期重启。

当今的处理器实现了这一技术,但是有情况下也不能做到。如果处理器预获取的数据中关键字是未知的。如果处理器在预获取期间请求缓存行,它就得等到接收到关键字时,才能调整接收到的字的顺序。
在这里插入图片描述
即使用上了这些优先措施,关键字在缓存行中的位置也很有影响。图3.30展示了下面的顺序访问和随机访问测试。图中显示的是指针位于第一个字时与位于最后一个字时的速度对比。元素大小是64字节,与缓存行大小一致。数据噪声大但是也可看出,当L2大小不足以放下工作集时,指针位于最后一个字的测试结果慢了约0.7%。顺序访问看起来受到的影响更大一点儿。这与前述的预获取下一缓存行会有的问题相符。

3.5.3缓存替换

缓存与超线程,核心,和处理器相关的地方是不受程序员控制的。但程序员可以决定线程执行,因此缓存与所使用的CPU的关系就变得重要了。

这里我们将讨论何时来选择用什么核心去运行线程。我们只描述程序员在设置线程亲和性时必须考虑的架构细节。

超线程,按定义来说会共享除寄存器集外的一切资源。包括L1缓存。这里也没有更多可以说的了。当处理器的每个核心独立出来时,事情开始变得有趣了。每个核心至少会有自己的L1缓存。除此之外,还有一些当今不太通用的细节:
• 早期的多核心处理器有分开的L2缓存,且没有更高级别的缓存了
• 后来的Intel模型有着共享L2缓存的双核处理器。对于4核处理器来说,每2个核心一对,每对必须有不同的L2缓存,且没有更高级别的缓存。
• AMD家族的10h处理器有分开的L2缓存和统一的L3缓存
在处理器厂商的宣传手册中有大量关于各个模型的优势的信息。如果由不同核心处理的工作集不重叠,则分开的L2缓存是有优势的。单线程程序可以运行得很好。因为到现在也经常是用到单线程程序,所以这一方式也没有很差。但总是有一些重叠。所有的缓存中都包含着通用的运行时类库最常用的部分,所以浪费了一些缓存空间。

像Intel的双核处理器那样完全共享所有的缓存有一个巨大优势。如果在2个核心上运行的线程使用的工作集很重叠,总的可用内存缓存就增加了,这样可以在工作集增大的情况下性能也不下降。如果工作集不重叠,Intel的高级智能缓存管理也能防止一个核心对缓存的独占。

但如果2个核心使用一半的缓存在它们各自的工作集上,就会有一些冲突。缓存必须持续衡量2个核心的使用情况,作为重新平衡一部分的缓存驱逐可能会选择不当。我们看看另一个测试程序结果来研究这个问题。
在这里插入图片描述
测序程序中有一个线程在持续地读或写,使用的是SSE指令集,内存块大小为2M。选择2M是因为这是Core 2处理器L2缓存的一半。第一个进程固定在一个核心上,第二个固定在另一个上。第二个线程读或写一块变化大小的内存区域。图展示了每时钟周期读或写的字节数。有4条不同的曲线,每一条都是读与写进程的组合结果。读/写曲线是后台进程的运行结果,进程在写时都使用了2M的工作集,读时则使用了大小变化的工作集。

曲线中有趣的部分在220-223字节段。如果2个核心 的L2缓存是完全分开的,我们可以预期4个测试都将在221-222字节段下降,因为那时L2缓存耗尽了。但从图3.31中看到不是这样。在后台进程中进行写操作的用例中这一现象是最能观察到的。性能在工作集还没到1M大小时就开始下降了。这时2个进程不共享内存所以处理器不用生产RFO消息。这完全是由缓存驱逐造成的。智能缓存处理存在其问题,即每个核心的经验缓存大小更接近1M,而不是可用的2M。我们只能寄希望于如果未来处理器都是共享缓存的话,智能缓存处理的算法能修复。

在能引用更高层级的缓存之前,四核处理器配置2个L2缓存只是权宜之计。这一设计相较于分开插槽的双核处理器没有明显的优势 。两个核心通过同一条总线进行通信,从外部看,这条总线就是FSB。没有特殊的快速数据交换。

多核处理器缓存设计的未来在于更多层次。AMD的10h处理器家族开了个头。但我们还是会看到处理器的核心子集共享低级别的缓存。额外的高级别缓存是必须的,因为高速又频繁被使用的缓存是不能在多核心间共享的。不然性能会受影响。将来也会需要有关联性的非常大的缓存。性能,缓存大小,关联性都必须随着共享缓存的核心数而扩展。使用一个大的L3缓存和相对合理大小的L2缓存是一个不错的权衡之计。L3缓存更慢,但它不像L2那样频繁地被使用。

对于程序员来说,这些不同的设计为规划增加了复杂性。需要知道负载和机器架构的细节来达到最佳性能。幸运的是在选择机器架构时,我们有一些助力。在后面的章节中我们会介绍相关接口。

3.5.4 FSB的影响

FSB对机器的性能至关重要。缓存内容可以在内存允许连接时即刻存储或加载。我们将在2台仅有不同速度的内存模块的区别的机器上运行一个程序,来看看这一影响有多深远。图3.32展示了Addnext0(将下一元素的pad[0] 加在当前pad[0] 上)测试的在NPAD=7,64位机器上的结果。两台机器都是Intel Core 2处理器,第一个使用了667MHz的DDR2模块,而第2个使用的是800MHz的模块(20%的提升)。
在这里插入图片描述
数据显示,当大工作集给FSB相当大的压力时,我们的确可见一个很大的好处。测试中可测量到的最大的性能增益是18.2%,接近于理论极限。更快的FSB的的确确可以节省大量时间。当工作集不大于缓存大小(这些处理器有4M的L2缓存)时还不关键。要记住这里我们测量的是一个程序。系统所需的内存是由并行运行的线程的工作集组成的。因此在很多更小的程序运行情况下,很容易超过4M或更多。

当今一些Intel处理器支持速度高达1333MHz的FSB,也就是60%的提升。未来将看到更高的速度。如果速度是重要的,并且工作集更大了,快速的RAM和高速FSB当然是值得花钱的。但人们还得注意,即使处理器可支持更高速的FSB,但主板/北桥可能不支持。关键得检查规格说明。

继续Part 3(虚拟内存)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值