java并发编程之CPU缓存一致性和内存屏障

2 篇文章 0 订阅
2 篇文章 0 订阅

多处理器内存体系

在多处理器系统中,各个核心共享主存,各个核心可以同时访问主存。但是处理器核心访问主存延迟是很大的。cpu使用缓存子系统来避免访问主存带来的延迟,使cpu可以更加高效的处理指令,提升性能。一些访问速度很快,但是内存很小的缓存一般集成在cpu核心中,而访问速度慢一些,内存更大的缓存则是多个核心共享的。寄存器,缓存和主内存一起构建了内存体系。下面介绍一下Intel x86架构中的寄存器以及L1、L2还有L3缓存。

  1. 寄存器:在每个核心里面,都有寄存器,访问寄存器只需一个时钟周期,这是cpu执行最快的内存区域了。编译器会将本地变量以及函数参数分配在寄存器上面
  2. 写缓存(store buffer):写缓存位于核心和L1 cache之间,是一个FIFO的队列。有store buffer的cpu不会去执行写指令,而是把写指令交给store buffer去操作,cpu接着执行后面的指令。
  3. L1缓存:L1缓存是核心本地的一个缓存,被分为32k数据缓存和32k指令缓存,访问需要3个时钟周期
  4. L2缓存:L2缓存也是cpu核心的本地缓存,位于L1和L3缓存之间,其大小为256k,数据和指令同时存储。其主要功能是为L1和L3缓存提供高效的内存队列。L2缓存的延迟需要12个时钟周期
  5. L3缓存:位于同一插槽的核心共享L3缓存,L3拥有L1和L2缓存的数据,虽然浪费了空间,但是拦截了访问核心的L1和L2缓存,减轻了L1和L2的压力

在这里插入图片描述

为什么需要缓存一致性

假如说现在有core1和core2两个核心,在主存中存在int a = 0,core1进行写操作a = 1,但是cpu为了提升性能,不会将a=1写到主存,而是直接写到自己的cache(cpu操作主存很慢),core2这时候读取a = 0(主存中a=0),2个core互相观察对方,都是乱序执行的,在core1看来,指令顺序是store load(store是core1自己的写操作 a = 1,load是core2读取主存的操作a = 0),但是在core2看来,指令顺序是load(load是core2自己的读操作,那这里为什么没有core1的写操作?因为core2看不到,core之间只能看到对方对主存的操作),这里的乱序明显对core2造成了影响(没有读到最新的数据)。其实在并发编程里的各种问题,本质上都是指令乱序造成的。缓存一致性就是用来解决这种问题的(其实lock指令也可以解决这种问题,但是lock指令代价太大,其原理是锁住总线,其他cpu全部停止执行,只有当前cpu可以访问内存)
在这里插入图片描述

cpu缓存一致性MESI以及MESIF

下面我们来介绍MESI协议
在实现了MESI协议的处理器架构中,会提供缓存控制器来跟踪每个缓存行(x86的缓存行大小是64byte)的状态,这些状态分别为:

  1. 被修改(Modified):表示该缓存行已过期,接下来需要写到主内存,当写回主内存时,状态变为独占。只有本地cpu进行写操作才能导致状态变为被修改。
  2. 独占(Exclusive):该状态下,当前核心的缓存和主内存一致,当本地cpu进行写操作时,进入被修改状态
  3. 共享(Shared):该状态表示当前cpu的缓存和主内存一致,而且至少有一个其他的核心的缓存和当前cpu的缓存一致
  4. 无效(Invalid):表示当前cpu的缓存行失效(可以看成不存在该缓存行)

其中,导致状态变化的有2种动作,一种是本地cpu自己的操作(local read,local write),还有就是嗅探到其他cpu的动作(remote read,remote write)。下面我们列出这些操作会导致什么样的变化:

  • 本地读( local read)
    • 如果当前缓存行状态为Invalid,那么local read会导致当前缓存行状态变成Exclusive或者Shared。如果其他核心没有该数据和数据失效,那么状态变成Exclusive,如果其他核心的缓存有该数据,那么改成Shared。
    • 如果当前缓存行状态为Modified,状态保持Modified不变。
    • 如果当前缓存行状态为Exclusive,状态保持Exclusive不变。
    • 如果当前缓存行状态为Shared,状态保持Shared不变。
  • 本地写(local write)
    • 如果当前缓存行状态为Invalid,那么当前缓存行状态变成Modified。
    • 如果当前缓存行状态为Modified,状态不变。
    • 如果当前缓存行状态为Exclusive,状态改成Modified。
    • 如果当前缓存行状态为Shared,状态改成Modified。
  • 嗅探到其他cpu读(remote read)
    • 如果当前缓存行状态为Invalid,状态不变。
    • 如果当前缓存行状态为Modified,将数据写回主内存,状态改成Shared。
    • 如果当前缓存行状态为Exclusive,状态改成Shared。
    • 如果当前缓存行状态为Shared,状态不变。
  • 嗅探到其他cpu写(remote write)
    • 如果当前缓存行状态为Invalid,状态不变。
    • 如果当前缓存行状态为Modified,状态改成Invalid。
    • 如果当前缓存行状态为Exclusive,状态改成Invalid。
    • 如果当前缓存行状态为Shared,状态改成Invalid

MESI看起来很不错,解决了一致性问题。但是不足的地方也很明显,当某个数据失效的cpu读取数据时,每个Shared状态的cpu核心都会应答,这是不必要的,于是,Intel x86架构的cpu使用了MESIF协议,其中的F是Forward,也就是说,在系统中,只有一个是F状态,当某个新来或者数据失效的cpu发生读时,F原来持有者变成S,读的cpu的状态变成F,F永远是最新读的那个。在cpu读的过程中,只有F应答。
既然cpu已经提供了缓存一致性,那么在并发编程过程中,是否就可以保证内存可见性了呢?答案是不能,原因如下:

  1. 编译器为了优化性能,会将变量在寄存器存很长时间,如果要让其他核能够看见该变量,那么变量就不能在寄存器中分配,使用内存屏障可以解决这个问题,而Java的volatile也能保证变量不会在寄存器分配。
  2. 如果cpu和cache之间没有store buffer,而是直接由cpu来进行写操作,那么为了满足缓存一致性协议,cpu在写完之后,必须给其他cpu发送缓存失效的消息,然后等待其他core的响应(其他core把缓存改成无效后才响应),这样非常浪费cpu资源。store buffer就是用来解决这个问题的,cpu把写指令交给store buffer,然后接着执行后面的指令。由于store buffer是FIFO的,所以对于store store指令不会进行重排序,但是假如是store load的话,那么就会乱序了,当cpu进行写操作时,将写指令交给store buffer,但是还没有写入cache,那么读操作就不能读到store buffer的最新值了,为了解决这个问题,cpu读操作会从cache和store buffe里面读,那么在同一个core里面的store load操作是没问题的,但是在不同core之间:core1进行写操作,写操作交给core1的store buffer,但是没有写入core1的cache,这个时候core2是不会收到缓存失效的消息的(缓存控制器嗅探不到store buffer),然后core2进行读,这个时候读的就不是最新值了,所以说即使有了缓存一致性协议,也是无法保证并发编程中的的内存可见性的。

上面说的都是基于Intel x86架构的cpu,Intel x86架构的cpu内存模型已经非常强了,其保证了store store,load load,load store是不会重排序的,有数据依赖的操作也不会重排序,但是保证不了store load(原因上面已经说过了),其实如果没有store buffer,那么store load也是可以保证不会重排序的,但是性能太差了。而其他内存模型很弱的架构的cpu,除了store buffer,还实现了invalid queue(因为当store buffer满的时候,cpu还是需要去等待其他core的invalid响应,而invalid queue的作用是:当收到invalid消息时,把消息放入队列,马上response,然后该cpu会去处理队列里面的invalid msg。如果cpu不去处理队列里的消息,那么就会读到旧的数据)。

内存屏障

cpu缓存一致性无法保证内存可见性,而内存屏障就是用来解决这个问题的。大部分处理器都会提供以下3种内存屏障:

  1. load barrier:用于保证读到最新的数据,仅仅保存读操作
  2. store barrier:用来保证写操作会被其他core看到,仅仅保证写操作
  3. full barrier:保证指令之后的读写操作不会重排序到指令之前

Intel x86架构下的实现为:

  1. lfence:读屏障指令,保证load load操作不会乱序
  2. sfence:写屏障指令,保证store store操作不会乱序
  3. mfence:读写屏障指令,防止读写操作之间重排序,性能相对lfence和sfence来说差很多

Intel x86是强内存模型(相对于大多数其他架构的cpu而言),在不需要内存屏障的情况下就保证了Load Load,Store Store,Load Store操作不会重排序,那么lfence指令和sfence指令到底有什么用呢?sfence的作用是:等待执行完store buffer中的store,保证其他cpu能看到当前cpu的写操作,假如cpu使用了Invalid queue(很多弱内存模型的cpu都使用了Invalid queue),那么lfence的作用很明显,用来刷新Invalid queue,读取最新数据。但是Intel x86架构的cpu没有使用Invalid queue,那么x86提供的lfence指令的作用是什么呢。

总结

cpu缓存一致性并不能保证内存可见性,需要使用内存屏障来防止指令的重排序。intel x86的cpu的lock指令有和完全屏障一样的效果。java的volatile在x86cpu的平台上就是用的lock指令

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值