1. CPU高速缓存
在CPU的全部取指令周期中(程序计算),至少需要访问一次存储器(也就是我们所说物理内存上的数据)
通常需要多次访问存储器的取操作数或者保存结果,CPU处理计算的速度明显受限于访问存储器的限制
因此解决方案就是利用局部性原理,在CPU与物理内存之间提供一个容量小并且速度快的存储器,称为高速缓存
缓存是分“段”(line)的,一个段对应一块存储空间,大小是 32(较早的 ARM、90 年代 /2000 年代早期的 x86 和 PowerPC)、64(较新的 ARM 和 x86)或 128(较新的 Power ISA 机器)字节
高速缓存包含物理内存部分数据副本
cpu读取数据时将会先检查高速缓存中的数据是否存在,存在就返回,不存在就读取物理内存数据
L1 Cache: 一级缓存是CPU第一层高速缓存,分为指令缓存和数据缓存,一般服务器的CPU的L1缓存容量在32-4096kb,现在的L1 Cache都不能直接与内存直连传输数据
L2 Cache: 由于L1级别高速缓存容量的限制,为了再次提高CPU的运算速度,在CPU外部放置一个高速存储器,即二级缓存
L3 Cache: 现在的L3缓存都是内置的,主要是进一步降低内存延迟,提升处理器运算能力,一般是多核共享一个L3缓存
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状态解决了修改缓存数据之前告诉其他处理器的问题
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源创计划”,欢迎正在阅读的你也加入,一起分享。