你好,我是 shengjk1,多年大厂经验,努力构建 通俗易懂的、好玩的编程语言教程。 欢迎关注!你会有如下收益:
- 了解大厂经验
- 拥有和大厂相匹配的技术等
希望看什么,评论或者私信告诉我!

老规则,开篇先骂简书,简书就属于那种几天不骂就浑身痒痒的,希望简书快点倒闭,写的文章又无故被封了。希望书友可以转战 https://shengjk1.blog.csdn.net/?type=blog 关注我,后续万一简书倒闭了,我们之间也不会失联!
一、前言
之前有一篇文章java 线程之间通信-volatile 和 synchronized,简单提及了一下,但不够全面,某些地方也容易引起误解。也回顾了一下 java 多线程这一系列,计算机的基础知识聊的比较少,但想要更好的理解多线程以及为后续多线程的介绍做铺垫,所以有必要单独开一篇来聊一下 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 环境下可以使用命令如下来查看缓存信息:
lscpu
这将显示处理器的详细信息,包括缓存大小和缓存行大小。
-
使用性能分析工具:有些性能分析工具或调试工具可以提供有关硬件信息的详细报告。这些工具有时会包含有关处理器缓存的参数,可以查看缓存行大小和其他缓存相关信息。
通过以上方法中的任何一种,您应该能够获取有关服务器所用处理器的缓存行大小的信息。这种信息对于优化内存访问和数据布局有重要意义,特别是在需要优化高性能计算或多线程应用程序时。
这里我们用 CPU-Z 查看一下我的电脑

我们重点来看一下红框框柱的部分
红色方框内的数据表示的是处理器的缓存信息。
- 一级数据缓存:4 x 48 KBytes,12-way(12路)
- 一级指令缓存:4 x 32 KBytes,8-way(8路)
- 二级缓存:4 x 1.25 MBytes,20-way(20路)
-
三级缓存:8 MBytes,8-way(8路)
这些缓存大小和路数反映了处理器在存储和检索数据方面的性能。
- 一级数据缓存:用于快速访问频繁使用的数据,如程序代码和数据结构。较大的缓存大小和较高的路数意味着更快的数据访问速度。
- 一级指令缓存:用于存储和检索指令,对于提高指令执行效率至关重要。较小的缓存大小和较低的路数可能影响指令执行的速度。
- 二级缓存:位于一级缓存和主内存之间,用于存储更大块的数据,如多个程序或线程的数据集。较大的缓存大小和较高的路数有助于减少对主内存的访问,从而提高整体性能。
-
三级缓存:最大的缓存层,通常用于存储大量数据,如整个程序或多个进程的数据。较大的缓存大小和较高的路数可以显著提高数据的访问速度,尤其是在处理大型数据集时。
这些缓存的大小和路数直接影响到处理器的性能,尤其是对于那些需要大量数据处理的应用程序。例如,对于高性能计算、图形处理或多任务处理场景,具有较大缓存大小的处理器能够更好地满足需求。
让我们来详细解释一下“一级数据缓存:4 x 48 KBytes,12-way(12路)”这个描述。
分解描述
-
4 x 48 KBytes:
- 这里的“4”表示该处理器有4个独立的一级数据缓存。
- “48 KBytes”表示每个缓存的大小是48千字节(KBytes)。
所以,总共的一级数据缓存大小是4 x 48 KBytes = 192 KBytes
。
-
12-way(12路):
- “12-way”或“12路”描述的是缓存的组相联度(set associativity)。
- 在组相联缓存中,每个缓存行(cache line)可以被放置到一组中的任何一个位置。
组相联缓存
为了更好地理解“12-way”,我们需要了解一些缓存的基础知识。
缓存是由很多行组成的,每行通常包含一定数量的字节(例如,在x86架构中,一个缓存行通常是64字节)。这些行被组织成多个组,每个组包含多个“路”(ways)。
在12路的缓存中,每个组有12个位置可以存放数据。当缓存需要决定在哪里放置一个数据时,它会根据某种算法(如最少使用或最近最少使用)选择一个组,然后在那个组内的12个位置中找一个空位。
举例说明
假设我们有以下情况:
- 一级数据缓存大小为192 KBytes,分为4个48 KBytes的缓存。
- 每个缓存是12路的。
那么,每个48 KBytes的缓存可以分解为:
- 缓存行大小:通常为64字节。
-
每个缓存有多少行:
48 KBytes / 64 bytes = 768 行
。 -
每个组有多少行:因为缓存是12路的,所以
768 行 / 12 = 64 行
。
这意味着每个缓存被分成了64个组,每个组有12个位置(路)可以存放数据。
为什么这样设计?
使用多路缓存设计(而不是直接映射缓存或全相联缓存)是为了在以下两个目标之间取得平衡:
- 空间效率:与全相联缓存相比,组相联缓存需要的硬件较少,因为它不需要在整个缓存中搜索数据的位置。
-
冲突概率:与直接映射缓存相比,组相联缓存减少了缓存冲突的可能性,即多个频繁访问的数据块争用同一个缓存位置的情况。
总之,“一级数据缓存:4 x 48 KBytes,12-way”意味着该处理器有4个独立的一级数据缓存,每个缓存大小为48 KBytes,并且每个缓存是12路的,这有助于提高数据访问的速度和缓存效率。
2.2.5 Cpu Cache Line 在多线程中如何避免伪共享
假设缓存行大小为 64 byte 而需要缓存的数据只有 1 byte 时,这个缓存行的剩余空间(63 bytes)可以被其他数据或变量共享使用。这种现象称为缓存行的伪共享(False Sharing)。
在多线程环境中,如果多个线程同时操作同一缓存行中的不同数据时,即使它们操作的数据字节位置不同,也可能会因为缓存行的争用(contention)而导致性能下降。这是因为现代处理器为了优化数据访问通常会以缓存行为单位进行数据的加载和存储。即使你正在写入一个字节的数据,整个缓存行都会被锁定,并且任何对该缓存行中其他数据的修改都会导致缓存行的共享问题。为了避免这种情况,可以采用以下策略:
-
填充(Padding): ( JDK源码中存在很多这样的代码 )在数据结构中使用额外的空间来确保每个变量或数据块占据完整的缓存行。这样做可以避免不同变量共享同一个缓存行的情况。例如,如果你有一个只有 1 byte 的数据需要缓存,你可以创建一个大小为 64 byte 的数据结构来存储这个数据,并确保其他 63 bytes 不被其他变量使用。这样,即使其他变量也在同一缓存行内,它们也不会影响到这个 1 byte 数据的安全性。
-
使用原子操作: 在多线程环境中操作缓存行数据时,可以使用原子操作来确保操作的原子性,从而减少缓存行的争用情况。原子操作可以保证在操作过程中不会被其他线程打断,从而避免缓存行的伪共享问题。然而,原子操作通常比非原子操作更重并且成本更高,所以不应该过度使用。在涉及缓存行的临界区域使用原子操作是比较常见的做法。
通过合理的填充和使用原子操作等策略,可以有效地管理缓存行的共享情况,从而提高程序的性能并减少潜在的冲突和争用情况。
三、扩展
3.1 CPU Cache 为什么不用寄存器
CPU中的寄存器是一种速度最快、容量最小且最接近于处理器核心的存储器单元,主要用于临时存储处理器需要立即访问的数据、指令和中间结果。寄存器在计算机系统中扮演着关键的角色,具有以下作用:
-
快速访问:
- 寄存器的访问速度非常快,几乎与处理器核心的执行速度一致,因此用于存储寄存器中的数据可以立即被处理器访问和处理,无需延迟等待。
-
存储数据和指令:
- 寄存器用于存储处理器需要立即访问的数据,如运算时的操作数、中间结果、指令地址等。在执行指令时,处理器从寄存器中读取操作数,并将结果存储回寄存器。
-
数据传递和交换:
- 寄存器用于在执行指令过程中传递数据和中间结果。数据在寄存器之间传递的速度比在内存中传递要快得多,因此寄存器有助于提高计算和数据操作的效率。
-
寄存器组织:
- CPU中通常有多个寄存器,如通用寄存器、专用寄存器(如程序计数器、指令寄存器)、条件码寄存器等。这些寄存器组织成不同的寄存器文件,用于存储不同类型的数据和控制信息。
-
指令执行:
- 在指令执行过程中,寄存器用于存储指令操作数和中间结果,帮助CPU执行指令的运算、逻辑和数据移动操作。
-
优化性能:
- 通过将数据暂存在寄存器中,可以减少对主存的访问次数,减少访存延迟,从而提高整体计算机系统的性能。
-
程序控制:
- 一些特定的寄存器如程序计数器(PC)用于存储当前正在执行的指令的地址,控制指令执行的流程。
总的来说,寄存器在CPU中扮演着存储临时数据、指令和中间计算结果、数据传递、优化性能等关键角色,帮助处理器高效执行指令并完成计算任务。寄存器的快速访问速度和与处理器紧密的关联使其成为计算机系统中最重要和基础的存储器组件之一。
3.2 虚拟内存
在计算机系统中,虚拟内存是一种计算机系统内存管理技术,它使得程序认为它拥有连续的可用内存,但实际上,其数据可能分散存储在物理内存(RAM)和硬盘(虚拟内存文件)之间。虚拟内存为每个进程提供了一个独立且分离的内存空间,使得每个程序运行时都有自己的内存地址空间。
虚拟内存的实现基于硬件和操作系统的协作,它包括以下关键组成部分:
-
虚拟地址空间:每个进程看到的内存地址空间被称为虚拟地址空间。每个进程只能访问属于自己的虚拟地址空间,这个地址空间通常是从0开始的连续地址空间,但实际上,它并不与物理内存一一对应。
-
页面机制:将虚拟内存和物理内存分割成固定大小的页面(通常是4KB或8KB),操作系统负责将数据从磁盘加载到物理内存,或者根据需要将物理内存中的数据写回到磁盘。
-
页表:用于映射虚拟内存地址到物理内存地址的数据结构。每个进程都有自己的页表,页表中存储了虚拟页到物理页的映射关系。
-
页面置换算法:当物理内存不足时,操作系统需要选择哪些页面被置换到磁盘上以腾出空间。常见的页面置换算法包括最近最少使用(LRU)和先进先出(FIFO)等。
虚拟内存带来了一系列的好处,包括:
- 更大的可用内存:使得每个程序能够使用比物理内存更大的地址空间。
- 内存保护:每个进程的虚拟地址空间是隔离的,可以防止程序之间相互干扰。
- 更好的内存管理:通过虚拟内存,操作系统可以更好地管理内存资源,提高系统的稳定性和可靠性。
总的来说,虚拟内存是一种重要的内存管理技术,它抽象了物理内存并为每个进程提供一个独立的虚拟地址空间,以提高系统的性能和可靠性。
四、参考链接
http://www.uml.org.cn/xjs/202309204.asp?artid=25948
五、总结
本文详细介绍了CPU Cache的原理和实现方式。CPU Cache利用时间局部性和空间局部性原理,通过缓存命中和缓存未命中的方式来提高数据访问性能。CPU Cache是通过SRAM实现的,具有低延迟、高速度的特点。相比之下,寄存器速度更快但容量有限,无法直接替代缓存。CPU Cache在处理器设计中起着重要作用,能够提供更大的数据集合供处理器访问,并支持多核处理器的设计。
网友评论