从CPU缓存看缓存的套路你是否知道?

1、前言

不一样存储技术的访问时间差别很大,从 计算机层次结构 可知,一般状况下,从高层往底层走,存储设备变得更慢、更便宜同时体积也会更大,CPU和内存之间的速度存在着巨大的差别,此时就会想到计算机科学界中一句著名的话:计算机科学的任何一个问题,均可以经过增长一个中间层来解决。html

2、引入缓存层

为了解决速度不匹配问题,能够经过引入一个缓存中间层来解决问题,可是也会引入一些新的问题。现代计算机系统中,从硬件到操做系统、再到一些应用程序,绝大部分的设计都用到了著名的局部性原理,局部性一般有以下两种不一样的形式:数据库

时间局部性:在一个具备良好的时间局部性的程序当中,被引用过一次的内存位置,在未来一个不久的时间内极可能会被再次引用到。缓存

空间局部性:在一个具备良好的空间局部性的程序当中,一个内存位置被引用了一次,那么在不久的时间内极可能会引用附近的位置。学习

有上面这个局部性原理为理论指导,为了解决两者速度不匹配问题就能够在 CPU 和内存之间加一个缓存层,因而就有了以下的结构:操作系统

从CPU缓存看缓存的套路你是否知道?

 

3、什么时候更新缓存

在 CPU 中引入缓存中间层后,虽然能够解决和内存速度不一致的问题,可是同时也面临着一个问题:当 CPU 更新了其缓存中的数据以后,要何时去写入到内存中呢?,比较容易想到的一个解决方案就是,CPU 更新了缓存的数据以后就当即更新到内存中,也就是说当 CPU更新了缓存的数据以后就会从上到下更新,直到内存为止,英文称之为write through,这种方式的优势是比较简单,可是缺点也很明显,因为每次都须要访问内存,因此速度会比较慢。还有一种方法就是,当 CPU 更新了缓存以后并不立刻更新到内存中去,在适当的时候再执行写入内存的操做,由于有不少的缓存只是存储一些中间结果,不必每次都更新到内存中去,英文称之为write back,这种方式的优势是 CPU 执行更新的效率比较高,缺点就是实现起来会比较复杂。线程

上面说的在适当的时候写入内存,若是是单核 CPU 的话,能够在缓存要被新进入的数据取代时,才更新内存,可是在多核 CPU 的状况下就比较复杂了,因为 CPU 的运算速度超越了 1 级缓存的数据 I\O 能力,CPU 厂商又引入了多级的缓存结构,好比常见的 L一、L二、L3 三级缓存结构,L1 和 L2 为 CPU 核心独有,L3 为 CPU 共享缓存。设计

从CPU缓存看缓存的套路你是否知道?

 

若是如今分别有两个线程运行在两个不一样的核 Core 1 和 Core 2 上,内存中 i 的值为 1,这两个分别运行在两个不一样核上的线程要对 i 进行加 1 操做,若是不加一些限制,两个核心同时从内存中读取 i 的值,而后进行加 1 操做后再分别写入内存中,可能会出现相互覆盖的状况,解决的方法相信你们都能想获得,第一种是只要有一个核心修改了缓存的数据以后,就当即把内存和其它核心更新。第二种是当一个核心修改了缓存的数据以后,就把其它一样复制了该数据的 CPU 核心失效掉这些数据,等到合适的时机再更新,一般是下一次读取该缓存的时候发现已经无效,才从内存中加载最新的值。htm

4、缓存一致性协议

不难看出第一种须要频繁访问内存更新数据,执行效率比较低,而第二种会把更新数据推迟到最后一刻才会更新,读取内存,效率高(相似于懒加载)。 缓存一致性协议(MESI) 就是使用第二种方案,该协议主要是保证缓存内部数据的一致,不让系统数据混乱。MESI 是指 4 种状态的首字母。每一个缓存存储数据单元(Cache line)有 4 种不一样的状态,用 2 个 bit 表示,状态和对应的描述以下:blog

状态描述监放任务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,为了描述方便,简化一下只看其逻辑结构:

从CPU缓存看缓存的套路你是否知道?

 

单核读取步骤:Core 0 发出一条从内存中读取 a 的指令,从内存经过 BUS 读取 a 到 Core 0的缓存中,由于此时数据只在 Core 0 的缓存中,因此将 Cache line 修改成 E 状态(独享),该过程用示意图表示以下:

从CPU缓存看缓存的套路你是否知道?

 

双核读取步骤:首先 Core 0 发出一条从内存中读取 a 的指令,从内存经过 BUS 读取 a 到 Core 0 的缓存中,而后将 Cache line 置为 E状态,此时 Core 1 发出一条指令,也是要从内存中读取 a,当 Core 1 试图从内存读取 a 的时候, Core 0 检测到了发生地址冲突(其它缓存读主存中该缓存行的操做),而后 Core 0 对相关数据作出响应,a 存储于这两个核心 Core 0 和 Core 1 的缓存行中,而后设置其状态为 S 状态(共享),该过程示意图以下:

从CPU缓存看缓存的套路你是否知道?

 

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

从CPU缓存看缓存的套路你是否知道?

 

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

从CPU缓存看缓存的套路你是否知道?

 

至此,变量 a 在 CPU 的两个核 Core 0 和 Core 1 中回到了 S(共享)状态了,以上只是简单的描述了一下大概的过程,实际上这些都是在 CPU 的硬件层面上去保证的,并且操做比较复杂。

5、总结

如今不少一些实现缓存功能的应用程序都是基于这些思想设计的,缓存把数据库中的数据进行缓存到速度更快的内存中,能够加快咱们应用程序的响应速度,好比咱们使用常见的 Redis 数据库多是采用下面这些策略:① 首先应用程序从缓存中查询数据,若是有就直接使用该数据进行相应操做后返回,若是没有则查询数据库,更新缓存而且返回。② 当咱们须要更新数据时,先更新数据库,而后再让缓存失效,这样下次就会先查询数据库再回填到缓存中去,能够发现,实际上底层的一些思想都是相通的,不一样的只是对于特定的场景可能须要增长一些额外的约束。基础知识才是技术这颗大树的根,咱们先把根栽好了,剩下的那些枝和叶都是比较容易获得的东西了。

原文连接:https://www.cnblogs.com/mghio/p/13727341.html

若是以为本文对你有帮助,能够关注我一块儿学习进步,也能够关注我公众号,上面有更多技术干货文章以及相关资料共享,你们一块儿学习进步!