了解CPU,让代码更高效

353 阅读5分钟

CPU发展历史

摩尔定律是英特尔创始人之一戈登·摩尔的经验之谈,其核心内容为:集成电路上可以容纳的晶体管数目在大约每经过18个月便会增加一倍。换言之,处理器的性能每隔两年翻一倍

CPU的发展速度很快,运行效率越来越高,然而内存发展速度确慢的像蜗牛,CPU的厂商为了解决CPU读取内存慢的问题,引入了高速缓存集成在CPU中,从而间接提升CPU的整体运行效率。 常见的缓存为三级缓存:L1 L2 L3, 其中L1L2属于kb级别的 L3属于M级别的。如下图所示:

image.png

CPU读取效率

CPU为了提升效率于是引入的高速缓存,那么引入之后,实际的效果是如何的呢?

如下图所示,可以看到L1的读取效率比主存要快40-60倍之多。 image.png

从性能上讲,高速缓存的引入是真的香啊~但凡事有利有弊,那么引入高速缓存的弊端是什么呢? 缓存的一致性问题!当两个CPU都读取了同一个数据,那么就分别缓存了两份数据副本,如果修改了其中一个数据副本,那么就产生了数据不一致的情况了。

解决缓存不一致的问题

  1. 总线锁 所谓的总线锁, 就是使用处理器提供的lock#信号, 当处理器在总线上输出这个信号的时候, 其他处理器的请求将被阻塞住了, 那么该处理器将独占共享内存了。 通过锁定总线,让其他某个核心独占使用总线,这样的代价太大了, 总线被锁定后, 其他核心就不能访问内存了, 可能会导致其他核心短时间内停止工作。
  2. 锁定缓存 当处理器发出lock前缀的信号,锁定住缓存行对应的内存区域。 其他的处理器在这片内存区域锁定期间,无法对这篇内存进行相关的操作。相对于锁住总线,明显代价小,粒度更小了。

MESI协议

MESI协议是基于Invalidate的高速缓存一致性协议,并且是支持回写高速缓存的最常用协议之一

首字母缩略词MESI中的字母表示可以标记高速缓存行的四种独占状态 1、修改(M) 高速缓存行仅存在于当前高速缓存中,并且是脏的 - 它已从主存储器中的值修改(M状态)。在允许对(不再有效)主存储器状态的任何其他读取之前,需要高速缓存在将来的某个时间将数据写回主存储器。回写将该行更改为共享状态(S)。 2、独家(E)   缓存行仅存在于当前缓存中,但是干净 - 它与主内存匹配。它可以随时更改为共享状态,以响应读取请求。或者,可以在写入时将其改变为修改状态。 3、共享(S)   表示此高速缓存行可能存储在计算机的其他高速缓存中并且是干净的 - 它与主存储器匹配。可以随时丢弃该行(更改为无效状态)。 4、无效(I)   表示此缓存行无效(未使用)。

缓存行 (cache line)

L1L2L3 各级缓存最小的存储单元就是缓存行。换句话说,L1 就是由很多个缓存行组成的。一个缓存行大小通常为64字节。

提问:这样的new long[1024 * 1024][8],一个二维数组,横向遍历与纵向遍历有何不同? 关于缓存行,这其实是一个比较经典的问题了。

  1. 访问 data[0],CPU core 尝试访问 CPU Cache,未命中。
  2. 尝试访问主内存,操作系统一次访问的单位是一个Cache Line的大小—64字节,这意味着:既从主内存中获取到了 data[0] 的值,同时将 data[0] ~ data[7] 加入到了 CPU Cache 之中,for free.
  3. 访问 data[1]~data[7],CPU core 尝试访问 CPU Cache,命中直接返回。
  4. 访问 data[8],CPU core 尝试访问 CPU Cache,未命中。
  5. 尝试访问主内存。重复步骤 2

这个问题建议大家动手写个demo试试,我自己尝试了一下, 基于自己的电脑运行速度大致: 横向遍历40ms左右 纵向遍历90ms左右

伪共享

伪共享问题就像缓存行的影子,必须要提一下。

啥叫伪共享呢?伪共享是指多个线程同时读写同一个缓存行的不同变量时导致的 CPU 缓存失效。 如下图所示: image.png Core1 Core2 相互操作数组中不同的位置的值,导致了缓存行不停的失效,这样的话,肯定是大大的影响效率的。那么有什么方式可以化解呢?

缓存填充

当Core1读取Cell[0] 的时候,我们将缓存行后面的空间填满,这样Cell[0]就可以独占整个缓存行,这样就不会与Core2相互失效了。如下图所示:

image.png

java7中的实现:
abstract class AbstractPaddingObject{
    protected long p1, p2, p3, p4, p5, p6;// 填充
}

public class PaddingObject extends AbstractPaddingObject{
    public volatile long value = 0L;    // 实际数据
}

java8提供了注解:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface Contended {
    String value() default "";
}

关于缓存行填充的代码这里就不上传啦,大家有兴趣的可以写个demo跑一跑测一下,觉得有收获的帮忙点个赞咯,就酱紫吧。