CPU缓存浅析

从计算机的层次结构来说说缓存,我们都知道cpu和内存之间的运算速度存在着巨大的差异,为了解决速度不匹配问题,自然会想到增加一个中间层。

一、程序局部性原理

引入 Cache 的理论基础是程序局部性原理,包括时间局部性和空间局部性。即最近被CPU访问的数据,短期内CPU 还要访问(时间),比如对一维数组来说,访问了地址x上的元素,那么以后访问地址x+1、x+2上元素的可能性就比较高;被 CPU 访问的数据附近的数据,CPU 短期内还要访问(空间)。

CPU缓存(Cache Memory)位于CPU与内存之间的临时存储器,它的容量比内存小但交换速度快。在缓存中的数据是内存中的一小部分,但这一小部分是短时间内CPU即将访问的,当CPU调用大量数据时,就可避开内存直接从缓存中调用,从而加快读取速度。在CPU中加入缓存是一种高效的解决方案,这样整个内存储器(缓存+内存)就变成了既有缓存的高速度,又有内存的大容量的存储系统了。缓存对CPU的性能影响很大,主要是因为CPU的数据交换顺序和CPU与缓存间的带宽引起的。

因此如果将刚刚访问过的数据缓存在Cache中,那下次访问时,可以直接从Cache中取,其速度可以得到数量级的提高。

二、引入中间层

有了如上局部性原理的理论指导,为解决cpu和内存之间的速度不匹配问题就可以在二者之间加一缓存层,于是有下图结构:

三、缓存更新问题

虽然在引入缓存层后可以解决速度不一致问题,但也引入了一个问题:缓存更新问题。当cpu更新了缓存中的数据之后,要什么时间写入到内存中呢?    很容易想到的一个思路就是cpu更新了缓存之后就立即更新到内存中,这种方式称之为 write through 。这种方式的优点就是比较简单,但缺点也很明显,由于每次都需要访问内存,速度比较慢。另一种方案叫 write back,即cpu在更新了缓存之后不用马上更新到内存中,而是在适当的时机再更新到内存中,因为很多时候缓存中只是存储了一些中间结果,没有必要每次都更新到内存。这种方式有点是cpi执行更新的效率高,缺点是实现起来比较复杂,而且一旦更新的后的数据未被写入内存时出现系统掉电的情况,数据就会丢失。

第二种方式,如果是单核cpu,可以在缓存要被新进入的数据取代时,才更新到内存。如果是多核cpu情况就变得复杂了。由于cpu的运算速度超越了1级缓存的数据 I\O能力,cpu厂商又引入了多级缓存结构,常见的L1、L2、L3 三级缓存结构,L1和L2是cpu核心独有,L3位cpu共享缓存。如下图:

举个栗子:

如果现在 core0 和 core1 上分别有一个线程要对内存中的变量 i 进行加1操作,如果不做限制,可能会出现相互覆盖的情况。第一种解决方案就是:只要有一个核心修改了缓存的数据之后就立刻把内存和其他核心更新。第二种解决方案就是:当一个核心修改了缓存数据之后就把其他同样复制了该数据的cpu核心失效掉这些数据,等到合适的时机在更新,通常时候下一次读取该缓存的时候发现已经无效,才去内存中加载最新的数据。

四、为什么要有多级缓存

随着科技的发展,热点数据的体积越来越大,单纯的增加一级缓存的性价比已经很低了,于是cpu厂商又引入了多级缓存。二级缓存可以看做是一级缓存的缓冲器,一级缓存制造成本很高容量有限,二级缓存的作用就是存储那些cpu处理时需要用到但一级缓存无法存储的数据,同理三级缓存。L1、L2、L3 容量是递增的,制造成本递减。

 (1)第一级cache (L1)位于CPU芯片上并且运算于CPU工作频率;
        (2)第二级cache(L2)也位于芯片上比L1速度慢而体积大;
        (3)第三级cache(L3)位于CPU外部,是速度最慢、体积最大的存储器。

上图只是简单举个栗子,一级缓存的容量是最小的处理性能是最好的只要4个时钟周期(clock cycles) ,三级缓存的运算性能已近接近内存了,所以L1、L2和L3有着本质的区别。使用dmidecode命令可以查看缓存的容量。

五、什么是cache line

cache line可以理解为cpu缓存中的最小缓存单位,内存和高速缓存之间的数据移动的最小数据单位就是cache line(缓存行),目前主流的cpu的cache line的大小都是64Bytes,假设我们有一个512字节的一级缓存,那么按照64B的缓存单位大小来算,这个一级缓存所能存放的缓存个数就是512/64 = 8个。

六、缓存一致性协议

多核cpu多某个内存块进行读写时会引起冲突问题,也叫缓存一致性问题。多个处理器核心都能够独立地执行计算机指令,从而有可能同时对某个内存块进行读写操作,并且由于我们之前提到的回写和直写的Cache策略,导致一个内存块同时可能有多个备份,有的已经写回到内存中,有的在不同的处理器核心的一级、二级Cache中。由于Cache缓存的原因,我们不知道数据写入的时序性,因而也不知道哪个备份是最新的。还有另外一个一种可能,假设有两个线程A和B共享一个变量,当线程A处理完一个数据之后,通过这个变量通知线程B,然后线程B对这个数据接着进行处理,如果两个线程运行在不同的处理器核心上,那么运行线程B的处理器就会不停地检查这个变量,而这个变量存储在本地的Cache中,因此就会发现这个值总也不会发生变化。

为了保证正确性,一但某个核心更新了内存中的值,硬件就必须保证其他核心能读取到更新后的值,如何保证呢?就是下面我们说的缓存一致性协议。MESI是指4种状态的首字母,每个缓存存储数据单元(cache line)有4

种不同的状态,用2个bit表示,状态和对应的描述如下:

状态描述监听任务
M 修改 (Modified)该 Cache line 有效,数据被修改了,和内存中的数据不一致,数据只存在于本 Cache 中Cache line 必须时刻监听所有试图读该缓存行相对就主存的操作,这种操作必须在缓存将该缓存行写回主存并将状态变成 S(共享)状态之前被延迟执行
E 独享、互斥 (Exclusive)该 Cache line 有效,数据和内存中的数据一致,数据只存在于本 Cache 中Cache line 必须监听其它缓存读主存中该缓存行的操作,一旦有这种操作,该缓存行需要变成 S(共享)状态
S 共享 (Shared)该 Cache line 有效,数据和内存中的数据一致,数据存在于很多 Cache 中Cache line 必须监听其它缓存使该缓存行无效或者独享该缓存行的请求,并将该 Cache line 变成无效
I 无效 (Invalid)该 Cache line 无效无监听任务

 

我们来看看缓存一致性协议是如何进行读写操作的,假设有一个双核CPU,其逻辑结构如下:

单核读取步骤:Core 0 发出一条从内存中读取a的指令,从内存通过总线读取a到Core0的缓存中,因为此时数据只在Core0的缓存中,所以将Cache line 修改为 E(独享) 状态,

双核读取步骤:首先core0 发出一条从内存中读取a的指令,从内存通过总线读取a到core0的缓存中,然后cache line置为E状态,此时core1发出一条指令,也要从内存中读取a,当core 1试图从内存中读取a的时候,core0检测到了发生地址冲突(其他缓存读主存中该缓存行的操作),然后core0对相关数据做出响应,a存储于这两个核心core0和core1的缓存行中,然后设置其状态为S状态(共享),示意图如下:

假设此时 Core 0 核心需要对 a 进行修改了,首先 Core 0 会将其缓存的 a 设置为 M(修改)状态,然后通知其它缓存了 a 的其它核 CPU(比如这里的 Core 1)将内部缓存的 a 的状态置为 I(无效)状态,最后才对 a 进行赋值操作。该过程如下所示:

上图中内存中 a 的值(值为 1)并不等于 Core 0 核心中缓存的最新值(值为 2),那么要什么时候才会把该值更新到内存中去呢?就是当 Core 1 需要读取 a 的值的时候,此时会通知 Core 0 将 a 的修改后的最新值同步到内存(Memory)中去,在这个同步的过程中 Core 0 中缓存的 a 的状态会置为 E(独享)状态,同步完成后将 Core 0 和 Core 1 中缓存的 a 置为 S(共享)状态,示意图描述该过程如下所示:

七、缓存替换策略

Cache工作原理要求它尽量保存最新数据,当从主存向Cache传送一个新块,而Cache中可用位置已被占满时,就会产生Cache替换的问题。
常用的替换算法有下面三种。
(1) LFU
LFU(Least Frequently Used,最不经常使用)算法将一段时间内被访问次数最少的那个块替换出去。每块设置一个计数器,从0开始计数,每访问一次,被访块的计数器就增1。当需要替换时,将计数值最小的块换出,同时将所有块的计数器都清零。
这种算法将计数周期限定在对这些特定块两次替换之间的间隔时间内,不能严格反映近期访问情况,新调入的块很容易被替换出去。
(2)LRU
LRU(Least Recently Used,近期最少使用)算法是把CPU近期最少使用的块替换出去。这种替换方法需要随时记录Cache中各块的使用情况,以便确定哪个块是近期最少使用的块。每块也设置一个计数器,Cache每命中一次,命中块计数器清零,其他各块计数器增1。当需要替换时,将计数值最大的块换出。
LRU算法相对合理,但实现起来比较复杂,系统开销较大。这种算法保护了刚调入Cache的新数据块,具有较高的命中率。LRU算法不能肯定调出去的块近期不会再被使用,所以这种替换算法不能算作最合理、最优秀的算法。但是研究表明,采用这种算法可使Cache的命中率达到90%左右。
(3) 随机替换
最简单的替换算法是随机替换。随机替换算法完全不管Cache的情况,简单地根据一个随机数选择一块替换出去。随机替换算法在硬件上容易实现,且速度也比前两种算法快。缺点则是降低了命中率和Cache工作效率。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在 Go 中进行 CPU 缓存优化的主要思路是减少 CPU 缓存未命中率,即尽可能使用局部性较好的数据结构和算法,以减少 CPU 访问内存的次数。 以下是一些可以优化 CPU 缓存的技巧: 1. 使用数组而非切片:数组是一段连续的内存空间,能够更好地利用 CPU 缓存。而切片则可能会分散在内存的不同位置,增加 CPU 缓存未命中率。 2. 多使用结构体而非接口:结构体是一段连续的内存空间,而接口则需要额外的内存空间来存储类型信息。因此,多使用结构体可以减少 CPU 缓存未命中率。 3. 避免 false sharing:False sharing 指的是两个不同的变量在同一个 CPU 缓存行中,当其中一个变量被修改时,会导致整个 CPU 缓存行失效,使得另一个变量的访问效率下降。可以使用 padding 来解决 false sharing 的问题。 4. 使用 sync.Pool:sync.Pool 是一个对象池,可以重复利用已经分配的对象,避免频繁进行内存分配和垃圾回收,从而减少 CPU 缓存未命中率。 5. 避免内存分配:频繁进行内存分配会导致 CPU 缓存未命中率增加,因此可以通过预分配内存、复用对象等方式来避免内存分配。 6. 使用局部变量:局部变量存储在栈上,访问速度更快,可以减少 CPU 缓存未命中率。 需要注意的是,进行 CPU 缓存优化需要在保证代码可读性和可维护性的前提下进行,不要过度追求性能而导致代码难以理解和维护。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值