1. CPU高速缓存
  • 在CPU的全部取指令周期中(程序计算),至少需要访问一次存储器(也就是我们所说物理内存上的数据)

  • 通常需要多次访问存储器的取操作数或者保存结果,CPU处理计算的速度明显受限于访问存储器的限制

  • 因此解决方案就是利用局部性原理,在CPU与物理内存之间提供一个容量小并且速度快的存储器,称为高速缓存

  • 缓存是分“段”(line)的,一个段对应一块存储空间,大小是 32(较早的 ARM、90 年代 /2000 年代早期的 x86 和 PowerPC)、64(较新的 ARM 和 x86)或 128(较新的 Power ISA 机器)字节

  • 高速缓存包含物理内存部分数据副本

  • cpu读取数据时将会先检查高速缓存中的数据是否存在,存在就返回,不存在就读取物理内存数据

CPU高速缓存与内存屏障-LMLPHP

  • L1 Cache: 一级缓存是CPU第一层高速缓存,分为指令缓存和数据缓存,一般服务器的CPU的L1缓存容量在32-4096kb,现在的L1 Cache都不能直接与内存直连传输数据

  • L2 Cache: 由于L1级别高速缓存容量的限制,为了再次提高CPU的运算速度,在CPU外部放置一个高速存储器,即二级缓存

  • L3 Cache: 现在的L3缓存都是内置的,主要是进一步降低内存延迟,提升处理器运算能力,一般是多核共享一个L3缓存

CPU高速缓存与内存屏障-LMLPHP

2. 缓存一致性与MESI协议
  • 缓存读操作

    • CPU读取数据时,先在L1中寻找,再从L2中寻找,再从L3中寻找,然后是内存,最后是外存储器(持久化介质)

    • 如果只处理读取操作,那么不论是L1-L3的缓存,都将会和主内存的数据保持一致

  • 缓存写操作

    • 缓存直写:直接透过本级缓存,直接将数据写入到下一级缓存或主内存中,写入成功/失败后将对应的缓存内容也执行更新/丢弃操作,这样缓存中的数据与将与主内存数据一致

    • 缓存回写:将修改本级缓存并记录标志段,通过标志段将数据回写到下一级缓存或主内存中,如果标志段被丢弃也将会先进行一次回收,也保证所有级别的缓存数据保持一致

  • 多核缓存的写操作问题

    • 场景: 在一个多核且每核都有对应的缓存的处理器进行读写操作

    • 假设有一个CPU缓存了主内存上的某一段数据,在另一个CPU上需要对该内存段的数据进行写操作,此时在写数据的CPU更新了缓存而其他CPU并没有更新到缓存,这时候便会产生缓存数据的不一致性

如何解决上述的问题,出现缓存数据不一致的原因如下:
  • 多核CPU都有对应的高速缓存,每核缓存的数据都无法共享

  • 这时候我们会想到就是让缓存能够为多核CPU共享,但出现的问题就是处理器运算能力性能会下降,每次都需要等待其中一个CPU进行写操作之后才能够进行下一步的处理

  • 那么我们的期望就是使用多核缓存,同时也能够让它们运作变得像操作一组缓存一样,那么缓存的一致性协议就是为了这一点而设计解决问题的

缓存的一致性协议有很多种,比较典型的就是MESI协议,关于MESI协议简述如下:
  • 失效(Invalid)缓存段: 缓存不存在或者已经失效过时

  • 共享(Shared)缓存段: 数据有效,并与主内存以及其他Cache的数据保持一致,用于读缓存操作

  • 独占(Exclusive)缓存段: 数据有效,与主内存的数据保持一致,与S的区别就是在于该处理器处于独占的状态时,其他的cpu缓存将会失效

  • 已修改(Modified)缓存段,属于脏段,表示当前的cpu缓存已经修改过,但是还没有同步到主内存中,为当前的cpu所专有

  • 小结:也就是cpu控制缓存的读写操作,还需要监听其他cpu发生的通知,保证缓存最终数据的一致性,其中E状态解决了修改缓存数据之前告诉其他处理器的问题

CPU高速缓存与内存屏障-LMLPHP

3. 内存屏障
  • 为什么会出现指令重排序

CPU写缓存时发现区块正被其他CPU占用,为了提高CPU处理性能,可能将后面的读缓存命令优先执行
  • 指令重排原则

重排需要遵循as-if-serial语义规则,即不管怎么进行重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变.编译器/runtime/处理器都必须遵循as-if-serial语义,也就是说编译器和处理器不会对存在数据依赖关系的操作做重排序
  • 缓存中的数据与主内存的数据并不是实时同步的,各CPU(或CPU核心)间缓存的数据也不是实时同步的,也就是在同一个时间点,各CPU所看到同一个内存地址的数据的值可能是不一致的

  • 指令重排序存在问题,虽然遵循as-if-serial语义,但是仅仅能保证是在单核CPU下单线程自己执行的情况下保证结果是正确的,如果是多核多线程,指令逻辑无法分辨因果关联,可能会出现乱序,导致程序运行结果出现错误

  • 定义

是一类同步屏障指令,它使得CPU或编译器在对内存进行操作的时候,严格按照一定的顺序来执行, 也就是说在memory barrier之前的指令和memory barrier之后的指令不会由于系统优化等原因而导致乱序
  • 内存屏障指令

    • 写内存屏障,在指令后插入Store Barrier,能让写入缓存中最新的数据更新写入主内存,让其他线程可见.强制写入主内存,这种显示调用,CPU就不会因为性能考虑而去对指令重排

    • 读内存屏障,在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制从新主内存中加载数据读取主内存内容,让CPU缓存与主内存保持一致,避免缓存导致的一致性问题

    • 完全内存屏障,保障了早于屏障的内存读写操作的结果提交到内存之后,再执行晚于屏障的读写操作

  • 作用

    • 就是解决上述CPU高速缓存存在的问题

最后,关于写作内存相关的原因是为了更好地理解同步关键字synchronized的内存语义(下一篇哈)

本文分享自微信公众号 - 疾风先生(Gale2Writing)。
如有侵权,请联系 [email protected] 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

09-10 13:01