基础|01|CPU缓存知识(待完善0.3)

(写在前面,本文还没写完,争取在2022.2.1前写完,觉得可以的话,可以先关注噢)

概览

由于各存储结构的速度不同,容量和价格上也不同,因此

1、对于单个CPU产生了缓存架构

既然有了缓存,那么在多核中,怎么解决高速缓存一致性?

2、缓存一致性

MESI协议 确保了缓存一致性,该类型协议保证了多CPU的缓存之间同步

但该协议存在一些性能上的问题,因此,便有了Store buffer 机制,但Store buffer并不能保证变量写入缓存和主存的顺序 。

3、便有了内存屏障,该技术规定了一些操作必须在某些操作之后。

一、CPU缓存架构

各存储结构的速度比较

基础|01|CPU缓存知识(待完善0.3)

基础|01|CPU缓存知识(待完善0.3)

缓存物理架构

基础|01|CPU缓存知识(待完善0.3)

CPU读取存储器数据过程

1、CPU要取寄存器X的值,只需要一步:直接读取。

2、CPU要取L1 cachel的某个值,需要1-3步(或者更多):把cache:行锁住,把某个数据拿来,解

锁,如果没锁住就慢了。

3、CPU要取L2 cachel的某个值,先要到L1 cache里取,L1当中不存在,在L2里,L2开始加锁,加

锁以后,把L2里的数据复制到L1,再执行读L1的过程,上面的3步,再解锁。

4、CPU取L3 cachel的也是一样,只不过先由L3复制到L2,从L2复制到L1,从L1到CPU。

5、CPU取内存则最复杂:通知内存控制器占用总线带宽,通知内存加锁,发起内存读请求,等待

回应,回应数据保存到L3(如果没有就到L2),再从L3/2到L1,再从L1到CPU,之后解除总线锁

定。

寄存器并不每次数据都可以从缓存中取得数据,万一不是同一个内存地址中的数据,那寄存器还必须直接绕过缓存从内存中取数据。所以并不每次都得到缓存中取数据,这种现象有个专业的名称叫做缓存的命中率


二、缓存相关概念

缓存行

cache line 是缓存进行管理的一个最小存储单元,也叫缓存块。从内存向缓存加载数据也是按缓存块进行加载的,一个缓存块和一个内存中相同容量的数据块(下称内存块)对应。

缓存行大小通常为64byte。缓存行是什么意思呢?比如你的L1缓存大小是512kb,而cacheline=64byte,那么就是L1里有512*1024/64个cacheline

底层对于缓存行的管理存在很多方式,因为太过底层,先不记录,详细参考14 | CPU Cache:访存速度是如何大幅提升的?-极客时间

程序局部性

局部性是虚拟内存的基础,在程序运行时,可只装入部分程序的内存。局部性主要分为时间局部性和空间局部性,空间局部性简单来说就是在程序的一个存储位置被引用,那么其附近的位置也将被引用;

因此,在缓存结构中,通常会加载临近的内存都到缓存中(具体怎么加载?),也正因此,下面代码会存在一些性能上的差异

详细分析见 14 | CPU Cache:访存速度是如何大幅提升的?-极客时间

/**
当按行访问时地址是连续的,下次访问的元素和当前大概率在同一个 cache line
(一个元素 8 字节,而一个 cache line 可以容纳 8 个元素),
但是当按列访问时,由于地址跨度大,下次访问的元素基本不可能还在同一个 cache line,
因此就会增加 cache line 被替换的次数,所以性能劣化。
*/
a = new long[1024*1024][6];
//省略初始化过程
for(int i = 0; i < 1024*1024; i++) {   
  for(int j = 0; j < 6; j++) {   
    // 按行相加
    a[i][j]++;     
  } 
} 

for(int j = 0; j < 6; j++) {   
  for(int i = 0; i < 1024*1024; i++) {   
    //按列相加
    a[i][j]++;     
  } 
} 

伪共享

伪共享(false-sharing)的意思是说,当两个线程同时各自修改两个相邻的变量,由于缓存是按缓存块来组织的,当一个线程对一个缓存块执行写操作时,必须使其他线程含有对应数据的缓存块无效。这样两个线程都会同时使对方的缓存块无效,导致性能下降。

在Java中,解决伪共享通常有这些方法:

基础|01|CPU缓存知识(待完善0.3)

另外,在 JDK 1.8 中,提供了 @sun.misc.Contended 注解,使用该注解就可以让变量独占缓存行,不再需要手动填充了。 注意,JVM 需要添加参数 -XX:-RestrictContended 才能开启此功能。


在多核情况下,每个核都有对应缓存,如果有一个 CPU 修改了内存中的某个值,那么怎么确保其他 CPU 能够感知到这个修改?

三、缓存一致性 & MESI协议

缓存写策略

当 CPU 修改了缓存中的数据后,这些修改什么时候能传播到主存?解决这个问题有两种策略:写回(Write Back)和写直达(Write Through)。


到这里就是乱码的啦,还没写完,下次继续

链接:

这里怎么和Redis 的 AOF 以及

缓存一致性问题

通过该协议,确保多核缓存之间的一致性

对于主流的CPU来说,缓存的写操作基本上是两种策略(参看《缓存更新的套路》),

  • 一种是Write Back,写操作只要在cache上,然后再flush到内存上。
  • 一种是Write Through,写操作同时写到cache和内存上。

为了提高写的性能,一般来说,主流的CPU(如:Intel Core i7/i9)采用的是Write Back的策略,因为直接写内存实在是太慢了。

好了,现在问题来了,如果有一个数据 x 在 CPU 第0核的缓存上被更新了,那么其它CPU核上对于这个数据 x 的值也要被更新,这就是缓存一致性的问题。(当然,对于我们上层的程序我们不用关心CPU多个核的缓存是怎么同步的,这对上层的代码来说都是透明的) 。

MESI协议

基础|01|CPU缓存知识(待完善0.3)

15 | MESI协议:多核CPU是如何同步高速缓存的?-极客时间

MESI协议 存在的性能问题,怎么处理?

四、Store buffer&内存屏障

Store buffer

store buffer 也会有一个问题,那就是它并不能保证变量写入缓存和主存的顺序

内存屏障

屏障的作用是前边的读写操作未完成的情况下,后面的读写操作不能发生


参考

1、编程高手必学的内存知识-极客时间

2、与程序员相关的CPU缓存知识 | 酷 壳 - CoolShell

3、记住这两幅重要的图!-码农翻身

上一篇:(计算机组成原理题目题型总结)第一章:计算机系统概述:题目


下一篇:Redis的过期删除策略(和内存淘汰机制)-转