多线程时如何使用CPU缓存?
作者:shengjk1
https://juejin.cn/post/7390480675172335666
一、前言
计算机的基础知识聊的比较少,但想要更好的理解多线程以及为后续多线程的介绍做铺垫,所以有必要单独开一篇来聊一下 CPU cache。
二、CPU
前面有一篇文章关于 CPU是如何进行计算 感兴趣的同学,可以先移步了解一下,不了解也没有关系,不影响这这篇文章的理解。
2.1 CPU 发展史以及为什么需要 cpu cache
时间回到1978年,第一颗在个人PC上使用的微处理器——8088,它的主频才4.77MHz,导致当时CPU的存取时间(800ns左右)远大于内存的存取时间(200ns左右),所以那时候根本不需要Cache。
到80386开始,CPU的频率一下提高到40MHz,但是内存的上升速度却没有象 CPU一样快,导致没有相匹配的内存可以使用,使得CPU要耗费几个,十几个时钟周期来等待内存的读写,这显然不能让人接受,于是有两种解决方案同时被提出来:一种是在CPU内加入等待周期,降低CPU的处理能力;而另一种就是寻找一种速度快,面积小,延迟短的存储体来做CPU与内存的中转站,也就是我们现在所说的Cache(缓存)。第一种方法显然是自欺欺人,牺牲CPU的性能来换取整体的平衡,所以第二种方法立刻被采用。
不过在386时期,由于成本的问题,并没有内部L1 Cache,只有外部的Cache。而486时代,CPU频率再次增加,外部Cache的速度也要相应提高,使得成本太高,于是内嵌了8K的L1 Cache,同时也可以使用外部的L2 Cache。
直到Pentium时代,由于Pentium采用了双路执行的超标量结构,有2条并行整数流水线,需要对数据和指令进行双重的访问,为了使得这些访问互不干涉,于是出现了8K数据Cache和8K指令Cache,并且可以同时读写,不过L2还是外部的,接着出现的Pentium Pro为了提高性能,把L2内嵌了,到此为止,就确定了现代缓存的基本模式了,并一直沿用至今。
这里还有一些关于 cacha 的访问数据,从数据中可以看出 L1 cache 相比于访问主存,性能提高 200 倍
2.2 CPU Cache 是什么
通常cpu内有3级缓存,即L1、L2、L3缓存。其中L1缓存分为数据缓存和指令缓存,cpu先从L1缓存中获取指令和数据,如果L1缓存中不存在,那就从L2缓存中获取。每个cpu核心都拥有属于自己的L1缓存和L2缓存。如果数据不在L2缓存中,那就从L3缓存中获取。而L3缓存就是所有cpu核心共用的。如果数据也不在L3缓存中,那就从内存中获取了。当然,如果内存中也没有那就只能从硬盘中获取了。
2.2.1 CPU Cache 原理
CPU Cache利用时间局部性和空间局部性原理来优化数据访问性能。这两个原理在Cache工作中扮演关键角色,以下是对CPU Cache工作原理与时间局部性、空间局部性的详细解释:
时间局部性:
概念:时间局部性意味着一旦数据被访问,它在不久的将来会再次被访问。
Cache应用:当处理器访问一个数据后,该数据会被保留在Cache中,因为存在时间局部性,表示该数据在短期内可能再次被访问。
优势:通过利用时间局部性,Cache能够减少对主存的访问次数,提高数据访问速度,因为处理器在未来访问相同数据时可以直接从Cache中获取。
空间局部性:
概念:空间局部性表明一旦访问了一个数据,其临近的数据也有可能被访问。
Cache应用:由于空间局部性,Cache可能会预先加载邻近数据块,以提高整体的Cache命中率。
优势:通过存储相邻数据块,Cache利用空间局部性可以更有效地提供处理器可能需要的数据,减少Cache未命中的情况。
工作原理:
缓存命中:当处理器请求数据时,首先在Cache中查找。如果数据存在于Cache中,发生缓存命中,处理器直接从Cache读取数据。
缓存未命中:如果请求的数据未在Cache中找到,发生缓存未命中,需要从主存加载数据到Cache中。
时间局部性应用:当数据被访问并存储在Cache中,根据时间局部性,该数据在不久的将来可能再次被访问。
空间局部性应用:根据空间局部性,Cache可能会预取与即将被访问的数据相关的邻近数据块,以提前加载可能被访问的数据。
优化策略:
数据块大小:选择适当的数据块大小以最大化空间局部性的利用。
替换策略:设计针对时间局部性的替换策略,保留最近使用的数据。
预取策略:根据空间局部性预先加载可能被访问的数据,以提高Cache命中率。
综合利用时间局部性和空间局部性原理,CPU Cache能够极大地优化数据访问性能,减少主存访问延迟,提高处理器的运行效率,是计算机系统中至关重要的组件之一。
2.2.2 CPU Cache 实现
2.2.2.1 CPU Cache 是通过 SRAM 实现的
在现代计算机中,我们最熟悉的半导体存储体有三种:
用于存储BIOS信息的EEPROM(Electrically Erasable Programmable Read Only Memory,电可擦写可编程只读存储器)
用于存储临时工作数据的DRAM(Dynamic Random Access Memory,动态随机访问存储器)
SRAM(Static Random Access Memory,静态随机访问存储器)。
EEPROM由于成本过高,速度太慢,肯定不能做为Cache材料的选择,而DRAM和SRAM两种,为什么是SRAM用来做Cache,而不是DRAM呢?当然最大的原因是速度。
我们知道DRAM目前最常见的应用就是内存了,它是利用每个单元的寄生电容来保存信号的,正因如此,它的信号强度就很弱,容易丢失,所以DRAM需要时时刷新来确定其信号(根据DRAM制造商的资料,DRAM至少64ms要刷新一次,并且由于每次读取操作都会破坏DRAM中的电荷,所以每次读取操作之后也要刷新一次,这就表示,DRAM至少有1%的时间是用来刷新的)。这样,DRAM的存储周期就增大了,当然潜伏期也变大了。我们从Cache的起源可以看出,正是因为内存的读取时间过高而引入Cache的,所以DRAM不符合做Cache的标准。
而SRAM不是通过电容充放电来存储数据,而是利用设置晶体管的状态来决定逻辑状态,让其保存数据,并且这种逻辑状态和CPU本身的逻辑状态相象,换句话说,SRAM可以以接近CPU频率的速度来运行(在今年秋季的IDF上,Intel公布的65nm工艺的SRAM,可以���行在3.4GHz的频率上),再加上读取操作对SRAM是不具破坏性的,不存在刷新问题,大大的缩短了潜伏期。所以这种低延迟,高速度的SRAM才是Cache材料的首选。
2.2.2.2 为什么不用寄存器实现
虽然寄存器和缓存都用于存储数据以提高处理器的性能,但它们有不同的设计目的和工作方式,导致无法直接将寄存器用作缓存。这里解释为什么CPU不使用寄存器来替代缓存:
速度和容量:
速度:寄存器通常是CPU内部最快的存储器,访问速度非常快,但寄存器的容量非常有限。缓存虽然比主存慢,但容量更大,能够存储更多数据并且与处理器核心之间的传输速度也更接近。
容量:寄存器的数量非常有限,一般只有几十个甚至更少,而缓存可以容纳更多数据,提供更大的数据集合供处理器访问,利用了空间局部性和时间局部性的原理。
成本和复杂性:
成本:寄存器的成本非常高昂,因为它们需要在CPU内部直接连接到执行单元。在CPU中集成大量寄存器会显著增加芯片成本,而缓存虽然也会增加成本,但比寄存器便宜。
复杂性:设计一个大规模的寄存器集合管理和调度是非常复杂的,而缓存的管理和替换策略相对容易实现。
共享和处理器设计:
共享性:寄存器是每个处理器核心私有的,而缓存是共享的,多个核心可以共享同一级别的缓存。这种共享能够更好地支持多核处理器的设计。
处理器设计:处理器的设计通常会包含多级缓存结构,每个级别的缓存都有特定的目的和功能,以提供更好的性能均衡。寄存器是用于存储指令、数据和中间结果,而缓存是用于加速访问主存的部分数据。
综上所述,虽然寄存器和缓存都用于存储数据以提高处理器性能,但由于速度、容量、成本、复杂性和处理器设计等方面的考虑,CPU不直接使用寄存器作为缓存的替代品。不同的存储器层次结构在处理器设计中起着各自不同的重要作用。
对CPU cache 整体了解之后,我们就可以进一步了解高速缓存的内部细节了
2.2.2 CPU Cache 内部细节
CPU Cache 在读取内存数据时,每次不会只读一个字或一个字节,而是一块块地读取,这每一小块数据也叫 CPU 缓存行(CPU Cache Line)。也就是说CPU 缓存和内存之间交换数据的最小单位通常是缓存行(Cache Line)。缓存行是缓存中数据的最小存储单位,在CPU和主内存之间传输数据时,以缓存行为单位进行数据传输和管理。
这也是对局部性原理的运用,当一个指令或数据被拜访过之后,与它相邻地址的数据有很大概率也会被拜访,将更多或许被拜访的数据存入缓存,可以进步缓存命中率。
在现代计算机系统中,通常使用的缓存(Cache)类型主要分为三种:直接映射缓存(Direct-Mapped Cache)、多路组相连缓存(Set-Associative Cache)和全相连缓存(Fully Associative Cache)。不同的缓存类型有其特点和适用场景。至于具体哪种类型的缓存用于计算机的L1、L2或L3缓存,这取决于具体的处理器架构和制造商的实现。
以下是这三种缓存类型的简要介绍:
直接映射缓存(Direct-Mapped Cache):在这种缓存中,每个缓存行只能存储一个特定的主存地址或地址范围的数据。每个缓存行都有一个唯一的标签与之关联,用于确定数据是否存在于缓存中。这种映射方式比较简单和高效,适用于高速缓存系统。它有一个主要的缺点是缓存行的使用不够灵活,如果多个不同的地址都需要同一个缓存行大小的数据,可能会造成冲突和性能下降。
多路组相连缓存(Set-Associative Cache):在这种缓存中,一个缓存行可以存储多个可能的地址范围的数据。这种类型的缓存允许多个主存地址共享相同的缓存行,并可以通过一个或多个标签来标识每个缓存行中的不同数据。这种设计提供了更大的灵活性,但查找操作可能需要更多的时间,因为处理器需要在多组选项中做出选择。这在缓存使用较多时会减少命中时间偏差的变化,有利于提高平均命中率。在计算机中经常使用的是所谓的伪相联策略,结合直接映射和全相连策略的特点。
全相连缓存(Fully Associative Cache):在这种类型的缓存中,没有任何限制条件用于决定哪些地址范围的数据可以存储在同一个缓存行中。每个缓存行都可以存储任何地址的数据,这使得查找操作相对复杂且耗时较长。然而,这种灵活性使得全相连缓存能够在某些情况下实现最佳的性能优化。这种类型在现代计算机系统中使用较少。在实际的处理器设计中,不同层次的缓存可能采用不同的策略以满足性能和能效的平衡需求。比如现代计算机可能在多级缓存中采用不同的映射策略组合,以适应不同的应用场景和需求。因此,具体的实现取决于处理器的架构和制造商的选择。建议您查阅具体的处理器文档或参考相关技术文档来获取最准确的信息。
接下来一一解释一下:
2.2.2.1 直接映射缓存
直接映射缓存会将一个内存地址固定映射到某一行的cache line。
其思想是将一个内存地址划分为三块,分别是Tag, Index,Offset(这里的内存地址指的是虚拟内存)。将cacheline理解为一个数组,那么通过Index则是数组的下标,通过Index就可以获取对应的cache-line。再获取cache-line的数据后,获取其中的Tag值,将其与地址中的Tag值进行对比,如果相同,则代表该内存地址位于该cache line中,即cache命中了。最后根据Offset的值去data数组中获取对应的数据。整个流程大概如下图所示:
下面是一个例子,假设cache中有8个cache line,
对于直接映射缓存而言,其内存和缓存的映射关系如下所示:
从图中我们可以看出,0x00,0x40,0x80这三个地址,其地址中的index成分的值是相同的,因此将会被加载进同一个cache line。
试想一下如果我们依次访问了0x00,0x40,0x00会发生什么?
当我们访问0x00时,cache miss,于是从内存中加载到第0行cache line中。当访问0x40时,第0行cache line中的tag与地址中的tag成分不一致,因此又需要再次从内存中加载数据到第0行cache line中。最后再次访问0x00时,由于cache line中存放的是0x40地址的数据,因此cache再次miss。可以看出在这个过程中,cache并没有起什么作用,访问了相同的内存地址时,cache line并没有对应的内容,而都是从内存中进行加载。
这种现象叫做cache颠簸(cache thrashing)。针对这个问题,引入多路组相连缓存。下面一节将讲解多路组相连缓存的工作原理。
2.2.2.2 多路组相连缓存
多路组相连缓存的原理相比于直接映射缓存复杂一些,这里将以两路组相连这种场景来进行讲解。
所谓多路就是指原来根据虚拟的地址中的index可以唯一确定一个cache line,而现在根据index可以找到多行cache line。而两路的意思就是指通过index可以找到2个cache line。在找到这个两个cache line后,遍历这两个cache line,比较其中的tag值,如果相等则代表命中了。
下面还是以8个cache line的两路缓存为例,假设现在有一个虚拟地址是0000001100101100,其tag值为0x19,其index为1,offset为4。那么根据index为1可以找到两个cache line,由于第一个cache line的tag为0x10,因此没有命中,而第二个cache line的tag为0x19,值相等,于是cache命中。
对于多路组相连缓存而言,其内存和缓存的映射关系如下所示:
由于多路组相连的缓存需要进行多次tag的比较,对于比直接映射缓存,其硬件成本更高,因为为了提高效率,可能会需要进行并行比较,这就需要更复杂的硬件设计。
另外,如何cache没有命中,那么该如何处理呢?
以两路为例,通过index可以找到两个cache line,如果此时这两个cache line都是处于空闲状态,那么cache miss时可以选择其中一个cache line加载数据。如果两个cache line有一个处于空闲状态,可以选择空闲状态的cache line 加载数据。如果两个cache line都是有效的,那么则需要一定的淘汰算法,例如PLRU/NRU/fifo/round-robin等等。
这个时候如果我们依次访问了0x00,0x40,0x00会发生什么?
当我们访问0x00时,cache miss,于是从内存中加载到第0路的第0行cache line中。当访问0x40时,第0路第0行cache line中的tag与地址中的tag成分不一致,于是从内存中加载数据到第1路第0行cache line中。最后再次访问0x00时,此时会访问到第0路第0行的cache line中,因此cache就生效了。由此可以看出,由于多路组相连的缓存可以改善cache颠簸的问题。
2.2.2.3 全相连缓存
从多路组相连,我们了解到其可以降低cache颠簸的问题,并且路数量越多,降低cache颠簸的效果就越好。那么是不是可以这样设想,如果路数无限大,大到所有的cache line都在一个组内,是不是效果就最好?基于这样的思想,全相连缓存相应而生。
下面还是以8个cache line的全相连缓存为例,假设现在有一个虚拟地址是0000001100101100,其tag值为0x19,offset为4。依次遍历,直到遍历到第4行cache line时,tag匹配上。
全连接缓存中所有的cache line都位于一个组(set)内,因此地址中将不会划出一部分作为index。在判断cache line是否命中时,需要遍历所有的cache line,将其与虚拟地址中的tag成分进行对比,如果相等,则意味着匹配上了。因此对于全连接缓存而言,任意地址的数据可以缓存在任意的cache line中,这可以避免缓存的颠簸,但是与此同时,硬件上的成本也是最高。
2.2.3 Cpu Cache Line 空闲
无论是三种缓存类型的哪种,Cache Line 要想被使用则必须处于空闲状态。
当说一个cache line(缓存行)处于空闲状态时,通常是指该缓存行当前没有有效的数据或有效的标记。在多级缓存中,每个缓存行通常包含了用于存储数据的存储单元、标记(tag)用于标识缓存行中数据的来源(比如主存地址),以及其他控制信息。
以下是关于缓存行空闲状态和标识码的解释:
空闲状态:
当一个cache line 被说成“空闲时”,意味着该缓存行当前不包含有效的数据。这可能是因为该缓存行还未被加载、已经被替换出去、还没有被写入数据,或者数据已被清除等情况。
标识码(Tag):
在直接映射缓存或其他高速缓存设计中,每个缓存行都会有一个标识码(tag),用于标识该缓存行中数据在主存中的地址范围,并区分不同的缓存行。
当某个缓存行处于空闲状态时,其相应的标识码可能不存在或被标记为无效,因为没有合法的数据与之对应。
在高速缓存中,空闲状态的缓存行是指该缓存行当前没有有效的数据,可能会在后续的访问中被加载、写入或更新。标识码通常用于查找特定数据的方法,当缓存行处于空闲状态时,标识码可能会被标记为无效或未被使用,以便在未来的访问中正确地识别和处理迁移数据。
2.2.3 Cpu Cache Line 大小
CPU的缓存线(cache line)大小是在设计阶段确定的固定值,通常由CPU架构的设计者根据性能需求和成本考虑来确定。不同的处理器架构可能会采用不同大小的缓存线,常见的缓存线大小通常是64字节、128字节或更大。以下是一些常见的缓存线大小选项:
64字节:
许多现代处理器(如英特尔和AMD的一些架构)使用64字节的缓存线大小。这意味着每次从主存中加载数据时,处理器会将整个64字节的缓存行加载到缓存中,即使只需要其中的一部分数据。
128字节:
有些处理器采用128字节的缓存线大小,这种设计能够更大程度上利用空间局部性的特点,提供更多数据以供处理器在未来访问。
其他大小:
除了64字节和128字节,还有一些处理器架构可能选择其他大小的缓存线,具体取决于设计者的需求和考虑。
缓存线的大小主要受到以下因素的影响:
空间局部性:较大的缓存行可以更好地利用空间局部性,一次加载更多数据,减少对主存的频繁访问。
性能需求:较大的缓存行可能提供更好的性能,但也会增加缓存的开销和复杂性。
成本:较大的缓存行会增加对缓存存储器和总线带宽的需求,可能导致更高的硬件成本。
因此,选择缓存线的大小是一个权衡取舍的过程,需要考虑多个因素以找到最适合特定处理器架构和应用场景的大小。
2.2.3.1 如何查看服务器中 Cpu Cache Line 大小
要确定服务器的缓存行大小,可以采取以下几种方法:
查阅处理器规格:查看服务器所用处理器的规格说明书或相关文档。处理器制造商通常会在其技术规格文档中提供有关缓存的详细信息,包括缓存层次结构、缓存行大小等。通过查找处理器型号和规格信息,您应该能够找到缓存行的大小。
使用 CPU-Z 或类似工具:CPU-Z 是一种常用的免费工具,可以用来查看计算机的硬件信息,包括处理器型号、缓存大小和缓存行大小等。通过运行 CPU-Z 或类似的硬件信息工具,您可以查看服务器使用的处理器类型以及相关的缓存信息。
操作系统命令:在某些操作系统中,您可以使用特定的命令来查看系统的硬件信息,包括处理器的缓存相关信息。例如,在 Linux 环境下可以使用命令如下来查看缓存信息:
微信扫码关注该文公众号作者