阅读 163

cpu的高速缓存指的是什么,高速缓存可以加快cpu

引言在分析JDK8中新增的高并发原子累加器Striped64时,发现有一个“伪共享”的概念,要理解它,必须对CPU缓存有一定的了解,所以本文首先对CPU的缓存结构和一些相关术语进行了描述

CPU缓存的原理是众所周知的。 当今计算机时代,CPU的运算处理速度和内存的读写速度差异巨大,为了解决这一差异,充分利用CPU的使用效率,CPU缓存应运而生。 这是介于CPU处理器和内存之间的临时数据交换缓冲区。

CPU缓存和内存都是断电后马上消失的非永久性随机存储器RAM,与内存在物理上有什么区别吗? 有。 CPU缓存基本上由静态ram (SRAM )组成。 (有些CPU缓存由eDRAM配置了IBM的电源系列处理。 )内存常被称为DRAM,实际上是SDRAM,是DRAM的一种。 构成存储器的DRAM中只包含一个晶体管和电容器,集成度非常高,可以简单地实现大容量化,但为了用电容器存储信息,需要不断更新电容器的电荷,根据充放电的时间差进行DRAM的数据读写

构成高速缓存的SRAM比构成内存的DRAM的复杂度高很多,所以占用空间大,成本高,集成度低。 因此,在CPU进程下降的前期,CPU高速缓存无法在CPU内部集成,只是集成在主板上,但由于不需要刷新电路,所以具有读写速度快的优点。

如果说SRAM和DRAM的物理结构和性能的差异显示了CPU缓存的物理原理,那么时间局部性原理和空间局部性原理就是支撑CPU缓存的逻辑原理。 时间局部原理是被参照的存储器位置很可能在不久的将来被多次参照,空间局部原理是当某些存储器的位置不再被参照时,程序很可能参照该存储器位置附近的存储器的位置。

CPU高速缓存的层次结构最初将CPU高速缓存分类是因为CPU内部集成的CPU高速缓存已经不能满足高性能CPU的需要,而且由于制造过程上的限制,CPU内部不能大幅增加高速缓存的数量,所以主板上当时,CPU内部的缓存称为L1 Cache,CPU外部主板上的缓存称为L2 Cache,而一级缓存实际上是分别用于存储数据和执行数据的指令解码的一级数据缓存(Data Cache ) 以及一级指令缓存) Instruction Cache、I-Cache、L1i ),两者可以同时访问CPU,减少CPU的多核

早期的Intel和AMD似乎对最后一级缓存L2有不同的看法。 每个CPU核心都有独立的一级高速缓存L1,那么二级高速缓存L2呢? 虽然AMD的做法仍然是每个CPU核心使用独立的二级高速缓存,但Intel采用了一个CPU多个核心共享二级高速缓存的设计,也就是所谓的“智能高速缓存”技术,这确实比当时AMD的设计性能要好

随着制造流程的提高,L2也整合到了CPU缓存中,但之后大数据处理和游戏性能等需求的提高,高端CPU出现了l3缓存。 据说三级高速缓存的出现将CPU的性能提高到了爬坡般。 当然,在2018年的今天,拥有三级高速缓存不再是高端CPU的特权,一些特殊的CPU甚至出现了四级高速缓存。 当然,高速缓存的级数越多,性能并不会越好。 成为3级高速缓存时,由于从CPU的传输距离和自身容量的提高,抵消了CPU访问高速缓存和直接访问存储器带来的性能的提高,所以与其增加所谓的4级高速缓存,不如直接访问存储器如上所述,我大致介绍了CPU缓存的分层结构。 下图为《深入理解计算机系统》书中的CPU缓存和内存、硬盘存储的分层结构。

如图所示,在深入了解计算机系统的书籍中,将寄存器划分为L0级缓存,依次为L1、L2、L3、内存、本地磁盘、远程存储器。 越往上走,缓存存储容量越小,速度越快,成本也越高。 越往下存储容量越大,速度越慢,成本也越低。 从上到下,每一层都可以视为更低一层的缓存。 也就是说,按顺序类推,L0寄存器是L1级缓存,L1是L2级缓存。 每个层次的数据都来到下一个层次,因此每个层次的数据是下一个层次的数据子集。

在继续CPU高速缓存之前,在笔记本电脑上简要介绍使用CPU-Z显示的处理器信息。

上述信息也可以通过名为CoreInfo的工具查看https://docs.Microsoft.com/en-us/sysinternals/downloads/core info。 第一幅图显示了我有一个L1、L2和L3级CPU高速缓存。 右下角说明双核4线程是什么意思? 双核与以前的单核相似。 也就是说,一个CPU具有多个核,每个核具有独立的运算处理单元、控制器、寄存器、L1、L2高速缓存,最后的CPU高速缓存L3由一个CPU的多个核共享,由此,如下图所示,一个进程

那么,什么是四线程? 其实这被称为超线程技术,因为可以通过采用特殊的硬件指令来为一个逻辑核仿真另一个物理芯片,如果通过

Windows的设备管理查看处理器你会看到四个,其实有两个都是模拟出来的,这样做可以将CPU内部暂时闲置的资源充分“调动”起来,因为我们的CPU在运行一个程序时其实还有很多执行单元是被闲置的。模拟出一个核就是为了使用CPU一些空闲的地方(资源),充分榨取CPU的性能。但是模拟出来的内核毕竟是虚拟的,所以它会和被模拟的逻辑核共享寄存器,L1,L2,因此就算是双核四线程还是只有2个一级缓存L1,2个二级缓存L2,一个三级缓存L3,所以假如物理核与它的模拟核中的线程要同时使用同一个执行单元里的东西时,或者访问同一个缓存行数据,还是只能一个一个的来。


    那么双CPU或者双处理器呢?前面所说的双核心是在一个处理器里拥有两个处理器核心,核心是两个,但是其他硬件还都是两个核心在共同拥有,而双CPU则是真正意义上的双核心,不光是处理器核心是两个,其他例如缓存等硬件配置也都是双份的。一个CPU对应一个物理插槽,多处理器间通过QPI总线相连。我们常见的计算机(例如上面我的笔记本)几乎都是单CPU多核心的,真正的多CPU并不是个人PC所常用的。

 

CPU缓存的内部结构

对CPU缓存的层次结构有了了解之后,我们再深入进CPU缓存内部,看看它内部的结构。

 

 

 上图是一个CPU缓存的内部结构视图,来至《深入理解计算机系统》一书,结合上图我这里只做简单的说明,若要细致深入的了解请参考原书。原来,CPU缓存内部一般是由S组构成,这个S的大小与该缓存的存储大小寻址空间有关,然后每一组里面又有若干缓存行cache line,例如上图每一组有E行cache line,E等于2,每一个缓存行包含一个标记其是否有效的有效位和t个标记位,然后才是真正存储缓存数据部分有B个字节大小。整个缓存区的大小C=B*E*S.

而一个内存地址在做缓存查找的时候,首先中间的s位指明了应该放在哪一组,高位的t位指明位于组中的哪一行,低位的b位表示应该从缓存行中的多少个偏移开始读取,毕竟一个缓存行可以存放很多数据的,一般是64个字节。

    这里面,代表行数量的E等于1的时候称之为“直接映射高速缓存”,E等于C/B即一个组包含所有行的时候称之为“全相联高速缓存”,当1>E>C/B即缓存行数介于这之间时称之为“组相联高速缓存”。由于CPU缓存的空间一般很小,内存数据映射到CPU缓存的算法必然将导致有很多不同的数据将被映射放置到相同的缓存行,这种访问同一个缓存行的不同数据就将导致缓存不命中,需要重新到下一级缓存或内存加载数据来替换掉原来的缓存,这种不命中称之为“冲突不命中”,如果这种冲突不命中持续产生,我们将之称之为“抖动”。很显然,直接映射高速缓存每一组只有一行所以这种“抖动”将可能是很频繁的,而这显然也不是最高的缓存设计方案。而“全相联高速缓存”虽然能最大限度的解决这种“抖动”但是由于行数太多想要CPU能够快速的在比较大的缓存中匹配出想要的数据也是非常困难的,而且代价昂贵。所以它只适合做小的高速缓存。最后只有“组相联高速缓存”才是我们最佳的方案。

    在上面CPU-Z的截图中,我的CPU缓存就是采用的组相联高速缓存,L1/L2后面的8-way说明它们每一组有8行,L3有12行,L1 d/L1 i的缓存总大小都是是32KB(注意前面有个乘以2 其实就是指有两个核心),L2的缓存大小是256KB....

    一般缓存行的大小是64个字节(不包含有效位和标记位),即B等于64,其实我的这个笔记本也是,这在上面CPU-Z的第二张图中可以看到,这些信息还可以通过CoreInfo工具或者如果我们用Java编程,还可以通过CacheSize API方式来获取Cache信息, CacheSize是一个谷歌的小项目,java语言通过它可以进行访问本机Cache的信息。示例代码如下:

public static void main(String[] args) throws CacheNotFoundException {CacheInfo info = CacheInfo.getInstance(); CacheLevelInfo l1Datainf = info.getCacheInformation(CacheLevel.L1, CacheType.DATA_CACHE);System.out.println("第一级数据缓存信息:"+l1Datainf.toString());CacheLevelInfo l1Instrinf = info.getCacheInformation(CacheLevel.L1, CacheType.INSTRUCTION_CACHE);System.out.println("第一级指令缓存信息:"+l1Instrinf.toString());}

 打印结果如下:

第一级数据缓存信息:CacheLevelInfo [cacheLevel=L1, cacheType=DATA_CACHE, cacheSets=64, cacheCoherencyLineSize=64, cachePhysicalLinePartitions=1, cacheWaysOfAssociativity=8, isFullyAssociative=false, isSelfInitializing=true, totalSizeInBytes=32768] 第一级指令缓存信息:CacheLevelInfo [cacheLevel=L1, cacheType=INSTRUCTION_CACHE, cacheSets=64, cacheCoherencyLineSize=64, cachePhysicalLinePartitions=1, cacheWaysOfAssociativity=8, isFullyAssociative=false, isSelfInitializing=true, totalSizeInBytes=32768]

显然这是和实际相符的,cacheSets表面有64组,cacheCoherencyLineSize表明缓存行的大小为64字节,cacheWaysOfAssociativity表示一组里面有8个缓存行,totalSizeInBytes就是整个缓存行的大小32KB,L1数据/指令缓存大小都为:C=B×E×S=64×8×64=32768字节=32KB。 

 

CPU缓存的读写与缓存一致性协议

 首先是读,CPU执行一条读内存字w的指令时是从上往下依次查找的,下层查找到包含字w的缓存行之后,再由下层将该缓存行返回给上一层高速缓存,上一层高速缓存将这个缓存行放在它自己的一个高速缓存行中之后,继续返回给上一层,直到到达L1。L1将数据行放置到自己的缓存行之后,从被存储的缓存行中抽取出CPU真正需要的字w,然后将它返回给CPU。大概就是高速缓存确定一个请求是否命中,然后1)组选择;2)行匹配;3)字抽取。

    这里面有一个很重要的地方就是,CPU缓存在不命中的时候,向下层缓存请求的时候,返回的数据是以一个缓存行为单位的,并不是只返回给你想要的单个字,另外当出现不命中冲突的时候,会执行相应的替换策略进行替换。

最后,关于写分为两种请况:

          1.要写一个已经缓存了的字w,即写命中:首先更新本级缓存的w副本之后,怎么更新它的下一级缓存?最简单是“直写”,即立即将包含w的高速缓存行写回到第一层的缓存层, 这样做虽然简单,但是你知道CPU每时每刻可能都在进行写数据,如果大家都不停的写势必会产生很大的总线流量,不利于其他数据的处理;另一种方法称为“写回”,尽可能的推迟更新,只有当替换策略需要替换掉这个更新过的缓存行时才把它写回到紧接着的第一层的缓存中,这样总量流量减少了,但是增加了复杂性,高速缓存行必须额外的维护一个“修改位”,表明这个高速缓存行是否被修改过。

         2.写一个不在缓存中的字,即写不命中:一种是写分配,就是把不命中的缓存先加载过来,然后再更新整个缓存行,后面就是写命中的处理逻辑了;另一种是非写分配,直接把这个字写到下一层。

 

说到CPU缓存的写操作还有一个很重要的话题,那就是缓存一致性协议MESI。关于缓存一致性协议及其变种又是另一个繁杂的内容,而MESI其实仅仅是众多一致性协议中最著名的一个,其名字的得名也来至于该协议中对四种缓存状态的缩写简称,缓存一致性协议规定了如何保证缓存在各个CPU缓存的一致性问题:

以MESI协议为例,每个Cache line有4个状态,可用2个bit表示,它们分别是: 

状态

描述

M(Modified)

这行数据有效,但数据被修改了,和内存中的数据不一致,数据只存在于本Cache中。

E(Exclusive)

这行数据有效,数据和内存中的数据一致,数据只存在于本Cache中。

S(Shared)

这行数据有效,数据和内存中的数据一致,数据存在于很多Cache中。

I(Invalid)

这行数据无效。

 关于缓存一致性协议,由于其又是一个比较繁多的内容,我这里仅仅粗略的说一下我的理解,总之它是一种保证数据在多个CPU缓存中一致的手段,至于到底是什么样的手段,根据各个CPU厂商采用的一致性协议的不同而不同,以我目前的了解,主要有以下几种:

1. 当CPU在修改它的缓存之前,会通过最后一级缓存L3(因为最后一级缓存是多核心共享的)或者总线(多CPU跨插槽的情况)广播到其他CPU缓存,使其它存在该缓存数据的缓存行无效,然后再更改自己的缓存数据,并标记为M,当其他CPU缓存需要读取这个被修改过的缓存行时(或者由于冲突不命中需要被置换出去时),会导致立即将这个被修改过的缓存行写回到内存,然后其他CPU再从内存加载最新的数据到自己的缓存行。

2. 当CPU缓存采用“直写”这种一更改马上写回内存的方式更新缓存的时候,其他CPU通过嗅探技术,从总线上得知相关的缓存行数据失效,则立即使自己相应的缓存行无效,从而再下次读不命中的时候重新到内存加载最新的数据。

3. 当CPU修改自己的缓存行数据时,主动将相关的更新通过最后一级缓存L3或者总线(如果是多CPU跨插槽的情况)发送给其它存在相关缓存的CPU,使它们同步的更新自己的缓存到一致。

 

总之,达到CPU缓存一致性的手段层出不穷,并且通过以上3种方式,可以看到在处理缓存一致性问题的时候,如果是单CPU多核心处理器,那么总是免不了使用最后一级缓存L3来传递数据,而这还不是最糟糕的,当多处理器跨插槽的时候,数据还要穿过总线跨插槽进行传输以保证缓存一致性, 这对性能将是更严峻的考验。这种CPU缓存一致性带来的问题将是我们在文章开始提出的“伪共享”的根本所在,具体讲在下一章节进行说明。


文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐