CPU缓存的意义

  CPU往往需要重复处理相同的数据、重复执行相同的指令,如果这部分数据、指令CPU能在CPU缓存中找到,CPU就不需要从内存或硬盘中再读取数据、指令,从而减少了整机的响应时间。所以,缓存的意义满足以下两种局部性原理:

  • 时间局部性(Temporal Locality):如果一个信息项正在被访问,那么在近期它很可能还会被再次访问。
  • 空间局部性(Spatial Locality):如果一个存储器的位置被引用,那么将来他附近的位置也会被引用。

CPU三级缓存结构

  计算机在运行过程中,CPU与存储器之间的数据交换最为频繁。CPU是计算机中速度最快的部件,而存储器的工作速度要低于CPU,这就意味着,CPU读取存储器中的数据需要有等待过程。计算机科学家研究发现,如果把要执行的程序代码和数据先行调入CPU暂存,就能提高CPU的运行效率。为此设计者在CPU内部开辟出一个存储区域,用于存放将要执行的程序代码,它和CPU以相同的速度运行。这个CPU内部存储器叫做高速缓存器(Cache)。

  在CPU内的那一部分存储器叫做L1缓存(Level 1 Cache)。后来在CPU外部和存储器之间也使用了一种速度快于存储器但稍慢于CPU的L2缓存(Level 2Cache)。最新的CPU技术将L1、L2缓存都设计在CPU芯片内,外置的是L3缓存(Level 3 Cache)。缓存对CPU的运行效率有直接影响,对各级缓存的调度也是由CPU负责完成的。  

  三级缓存的结构如下图所示:

原图链接

  • L1缓存分成两种,一种是指令缓存,一种是数据缓存。L2缓存和L3缓存不分指令和数据;
  • L1和L2缓存在每一个CPU核中,L3则是所有CPU核心共享的内存;
  • L1、L2、L3的越离CPU近就越小,速度也越快;

带有高速缓存CPU执行计算的流程

  • 程序以及数据被加载到主内存
  • 指令和数据被加载到CPU的高速缓存
  • CPU执行指令,把结果写到高速缓存
  • 高速缓存中的数据写回主内存

CPU缓存一致性协议(MESI)

  MESI(Modified Exclusive Shared Or Invalid)是一种广泛使用的支持写回策略的缓存一致性协议。为了保证多个CPU缓存中共享数据的一致性,定义了缓存行(Cache Line)的四种状态,而CPU对缓存行的四种操作可能会产生不一致的状态,因此缓存控制器监听到本地操作和远程操作的时候,需要对地址一致的缓存行的状态进行一致性修改,从而保证数据在多个缓存之间保持一致性。

   MESI的维基百科

MESI定义的四种状态

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

注意: 对于M和E状态而言总是精确的,他们在和该缓存行的真正状态是一致的,而S状态可能是非一致的。如果一个缓存将处于S状态的缓存行作废了,而另一个缓存实际上可能已经独享了该缓存行,但是该缓存却不会将该缓存行升迁为E状态,这是因为其它缓存不会广播他们作废掉该缓存行的通知,同样由于缓存并没有保存该缓存行的copy的数量,因此(即使有这种通知)也没有办法确定自己是否已经独享了该缓存行。

多核缓存协同操作

假设有三个CPU A、B、C,对应三个缓存分别是cache a、b、 c。在主内存中定义了x的引用值为0。

双核读取执行流程:

  1. CPU A发出了一条指令,从主内存中读取x;
  2. CPU A从主内存通过bus读取到 cache a中并将该cache line 设置为E状态;
  3. CPU B发出了一条指令,从主内存中读取x;
  4. CPU B试图从主内存中读取x时,CPU A检测到了地址冲突。这时CPU A对相关数据做出响应。此时x 存储于cache a和cache b中,x在chche a和cache b中都被设置为S状态(共享);

修改数据执行流程:

  1. CPU A 计算完成后发指令需要修改x;
  2. CPU A 将x设置为M状态(修改)并通知缓存了x的CPU B, CPU B将本地cache b中的x设置为I状态(无效);
  3. CPU A 对x进行赋值;

同步数据执行流程:

  1. CPU B 发出了要读取x的指令;
  2. CPU B 通知CPU A,CPU A将修改后的数据同步到主内存时cache a 修改为E(独享);
  3. CPU A同步CPU B的x,将cache a和同步后cache b中的x设置为S状态(共享);

内存屏障(Memory barrier)

为什么会有内存屏障

  • 每个CPU都会有自己的缓存(有的甚至L1,L2,L3),缓存的目的就是为了提高性能,避免每次都要向内存取。但是这样的弊端也很明显:不能实时的和内存发生信息交换,分在不同CPU执行的不同线程对同一个变量的缓存值不同。
  • 用volatile关键字修饰变量可以解决上述问题,那么volatile是如何做到这一点的呢?那就是内存屏障,内存屏障是硬件层的概念,不同的硬件平台实现内存屏障的手段并不是一样,java通过屏蔽这些差异,统一由jvm来生成内存屏障的指令。

内存屏障是什么

  • 硬件层的内存屏障分为两种:Load BarrierStore Barrier即读屏障和写屏障。
  • 内存屏障有两个作用:
  1. 阻止屏障两侧的指令重排序;
  2. 强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。
  • 对于Load Barrier来说,在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制从新从主内存加载数据;
  • 对于Store Barrier来说,在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。

原文链接

乱序执行

  乱序执行(out-of-orderexecution):是指CPU允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理的技术。这样将根据各电路单元的状态和各指令能否提前执行的具体情况分析后,将能提前执行的指令立即发送给相应电路。

  这好比请A、B、C三个名人为晚会题写横幅“春节联欢晚会”六个大字,每人各写两个字。如果这时在一张大纸上按顺序由A写好”春节”后再交给B写”联欢”,然后再由C写”晚会”,那么这样在A写的时候,B和C必须等待,而在B写的时候C仍然要等待而A已经没事了。

  但如果采用三个人分别用三张纸同时写的做法, 那么B和C都不必须等待就可以同时各写各的了,甚至C和B还可以比A先写好也没关系(就象乱序执行),但当他们都写完后就必须重新在横幅上(自然可以由别人做,就象CPU中乱序执行后的重新排列单元)按”春节联欢晚会”的顺序排好才能挂出去。

  所以,CPU 为什么会有乱序执行优化?本质原因是CPU为了效率,将长费时的操作“异步”执行,排在后面的指令不等前面的指令执行完毕就开始执行后面的指令。而且允许排在前面的长费时指令后于排在后面的指令执行完。

CPU 执行乱序主要有以下几种:

  • 写写乱序(store store):a=1;b=2; -> b=2;a=1;
  • 写读乱序(store load):a=1;load(b); -> load(b);a=1;
  • 读读乱序(load load):load(a);load(b); -> load(b);load(a);
  • 读写乱序(load store):load(a);b=2; -> b=2;load(a);

总而言之,CPU的乱序执行优化指的是处理器为提高运算速度而做出违背代码原有顺序的优化。