操作系统--CPU Cache

1.CPU Cache 访问速度

操作系统--CPU Cache_第1张图片


2.CPU Cache 读取 内存 过程

CPU Cache 的数据是从内存中读取过来的,它是以一小块一小块读取数据的。

在 CPU Cache 中的,这样一小块一小块的数据,称为 Cache Line(缓存块)

在内存中,这一块的数据我们称为内存块(Block

它们的大小是一次载入数据的大小,服从顺序加载


3.CPU 读取 CPU Cache 过程

CPU 怎么知道要访问的内存数据,是否在 Cache 里?

如果在的话,如何找到 Cache 对应的数据呢?

从 内存地址 计算 CPU Cache地址:

  • 首先,读取内存块的时候我们要拿到数据所在内存块的地址。
  • 其次,使用直接映射 Cache 策略。把内存块的地址始终「映射」在一个 CPU Cache Line(缓存块) 的地址。映射关系实现方式:「取模运算」。
  • 最后,取模运算的结果就是内存块地址对应的 CPU Cache Line地址。

操作系统--CPU Cache_第2张图片

内存地址包括:

  1. 组标记:区别不同的内存块(使用取模方式映射的话,就会出现多个内存块对应同一个 CPU Cache Line)
  2. CPU Cache 索引:取模运算的结果
  3. 偏移量:CPU 在从 CPU Cache 读取数据的时候,并不是读取 CPU Cache Line 中的整个数据块,而是读取 CPU 所需要的一个数据片段,这样的数据统称为一个字(Word。那怎么在对应的 CPU Cache Line 中数据块中找到所需的字呢?答案是,需要一个偏移量(Offset)

CPU Cache数据结构:

  1. 索引
  2. 有效位:用来标记对应的 CPU Cache Line 中的数据是否是有效的,如果有效位是 0,无论 CPU Cache Line 中是否有数据,CPU 都会直接访问内存,重新加载数据。
  3. 组标记
  4. 数据块:从内存加载过来的实际存放数据(Data

从 CPU Cache地址 读取数据到 CPU:

如果内存中的数据已经在 CPU Cache 中了,那CPU 访问一个内存地址的时候,会经历这 4 个步骤:

  1. 根据内存地址中索引信息,计算在 CPU Cache 中的索引,也就是找出对应的 CPU Cache Line 的地址;
  2. 找到对应 CPU Cache Line 后,判断 CPU Cache Line 中的有效位,确认 CPU Cache Line 中数据是否是有效的,如果是无效的,CPU 就会直接访问内存,并重新加载数据,如果数据有效,则往下执行;
  3. 对比内存地址中组标记和 CPU Cache Line 中的组标记,确认 CPU Cache Line 中的数据是我们要访问的内存数据,如果不是的话,CPU 就会直接访问内存,并重新加载数据,如果是的话,则往下执行;
  4. 根据内存地址中偏移量信息,从 CPU Cache Line 的数据块中,读取对应的字。

4.如何写出让 CPU 跑得更快的代码?

访问的数据在 CPU Cache 中的话,意味着缓存命中,缓存命中率越高的话,代码的性能就会越好,CPU 也就跑的越快。于是,「如何写出让 CPU 跑得更快的代码?」这个问题,可以改成「如何写出 CPU 缓存命中率高的代码?」。

我们要分开来看「数据缓存」和「指令缓存」的缓存命中率

如何提升数据缓存的命中率?

按照内存布局顺序访问数据。不要跳跃式遍历数据。

如何提升指令缓存的命中率?

CPU 的分支预测器。对于 if 条件语句,意味着此时至少可以选择跳转到两段不同的指令执行,也就是 if 还是 else 中的指令。那么,如果分支预测可以预测到接下来要执行 if 里的指令,还是 else 指令的话,就可以「提前」把这些指令放在指令缓存中,这样 CPU 可以直接从 Cache 读取到指令,于是执行速度就会很快

因此,先排序再遍历速度会更快,这是因为排序之后,数字是从小到大的,那么前几次循环命中 if < 50 的次数会比较多,于是分支预测就会缓存 if 里的 array[i] = 0 指令到 Cache 中,后续 CPU 执行该指令就只需要从 Cache 读取就好了。

如何提升多核 CPU 的缓存命中率?

现代 CPU 都是多核心的,线程可能在不同 CPU 核心来回切换执行,这对 CPU Cache 不是有利的,虽然 L3 Cache 是多核心之间共享的,但是 L1 和 L2 Cache 都是每个核心独有的,如果一个线程在不同核心来回切换,各个核心的缓存命中率就会受到影响,相反如果线程都在同一个核心上执行,那么其数据的 L1 和 L2 Cache 的缓存命中率可以得到有效提高,缓存命中率高就意味着 CPU 可以减少访问 内存的频率。

线程绑定在某一个 CPU 核心上,这样性能可以得到非常可观的提升。


5.CPU 写直达 过程

数据不光是只有读操作,还有写操作,那么如果数据写入 Cache 之后,内存与 Cache 相对应的数据将会不同,这种情况下 Cache 和内存数据都不一致了,于是我们肯定是要把 Cache 中的数据同步到内存里的。

保持内存与 Cache 一致性最简单的方式是,把数据同时写入内存和 Cache 中,这种方法称为写直达(Write Through

操作系统--CPU Cache_第3张图片

写直达法很直观,也很简单,但是问题明显,无论数据在不在 Cache 里面,每次写操作都会写回到内存,这样写操作将会花费大量的时间,无疑性能会受到很大的影响。


6.CPU 写回 过程

在写回机制中,当发生写操作时,新的数据仅仅被写入 Cache Block 里,只有当修改过的 Cache Block「被替换」时才需要写到内存中,减少了数据写回内存的频率,这样便可以提高系统的性能。

操作系统--CPU Cache_第4张图片

那具体如何做到的呢?下面来详细说一下:

  • 如果当发生写操作时,数据已经在 CPU Cache 里的话,则把数据更新到 CPU Cache 里,同时标记 CPU Cache 里的这个 Cache Block 为脏(Dirty)的,这个脏的标记代表这个时候,我们 CPU Cache 里面的这个 Cache Block 的数据和内存是不一致的,这种情况是不用把数据写到内存里的;
  • 如果当发生写操作时,数据所对应的 Cache Block 里存放的是「别的内存地址的数据」的话,就要检查这个 Cache Block 里的数据有没有被标记为脏的:
    • 如果是脏的话,我们就要把这个 Cache Block 里的数据写回到内存,然后再把当前要写入的数据,先从内存读入到 Cache Block 里,然后再把当前要写入的数据写入到 Cache Block,最后也把它标记为脏的;
    • 如果不是脏的话,把当前要写入的数据先从内存读入到 Cache Block 里,接着将数据写入到这个 Cache Block 里,然后再把这个 Cache Block 标记为脏的就好了。

7.缓存一致性

现在 CPU 都是多核的,由于 L1/L2 Cache 是多个核心各自独有的,那么会带来多核心的缓存一致性(Cache Coherence 的问题,如果不能保证缓存一致性的问题,就可能造成结果错误。

要解决这一问题,就需要一种机制,来同步两个不同核心里面的缓存数据。要实现的这个机制的话,要保证做到下面这 2 点:

  • 第一点,某个 CPU 核心里的 Cache 数据更新时,必须要传播到其他核心的 Cache,这个称为写传播(Write Propagation
  • 第二点,某个 CPU 核心里对数据的操作顺序,必须在其他核心看起来顺序是一样的,这个称为事务的串行化(Transaction Serialization

基于总线嗅探机制的 MESI 协议,就满足上面了这两点,因此它是保障缓存一致性的协议。

总线嗅探(Bus Snooping

通过总线把这个事件广播通知给其他所有的核心,然后每个 CPU 核心都会监听总线上的广播事件,并检查是否有相同的数据在自己的 L1 Cache 里面。

优点:简单

缺点:总线负担加重、不能保证事务串行化

MESI 协议

  • Modified,已修改
  • Exclusive,独占
  • Shared,共享
  • Invalidated,已失效

这四个状态来标记 Cache Line 四个不同的状态。整个 MSI 状态的变更,则是根据来自本地 CPU 核心的请求,或者来自其他 CPU 核心通过总线传输过来的请求,从而构成一个流动的状态机。另外,对于在「已修改」或者「独占」状态的 Cache Line,修改更新其数据不需要发送广播给其他 CPU 核心。


8.参考

小林 coding

你可能感兴趣的:(操作系统学习记录,mybatis)