(写在前面,本文还没写完,争取在2022.2.1前写完,觉得可以的话,可以先关注噢)
概览
由于各存储结构的速度不同,容量和价格上也不同,因此
1、对于单个CPU产生了缓存架构
既然有了缓存,那么在多核中,怎么解决高速缓存一致性?
2、缓存一致性
MESI协议 确保了缓存一致性,该类型协议保证了多CPU的缓存之间同步
但该协议存在一些性能上的问题,因此,便有了Store buffer 机制,但Store buffer并不能保证变量写入缓存和主存的顺序 。
3、便有了内存屏障,该技术规定了一些操作必须在某些操作之后。
一、CPU缓存架构
各存储结构的速度比较
缓存物理架构
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中,解决伪共享通常有这些方法:
另外,在 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协议
15 | MESI协议:多核CPU是如何同步高速缓存的?-极客时间
MESI协议 存在的性能问题,怎么处理?
四、Store buffer&内存屏障
Store buffer
store buffer 也会有一个问题,那就是它并不能保证变量写入缓存和主存的顺序
内存屏障
屏障的作用是前边的读写操作未完成的情况下,后面的读写操作不能发生
参考
1、编程高手必学的内存知识-极客时间
2、与程序员相关的CPU缓存知识 | 酷 壳 - CoolShell