内存屏障的前世今生

137 阅读4分钟

内存屏障的前世今生

从cpu的发展进程讲起

早期单核cpu

特点: 单核,按照程序指令顺序进行计算

性能优化:

  • 乱序执行:让 CPU 可以不按程序的顺序执行指令,从而利用 CPU 的空闲资源,提升执行效率(重排序
  • 提升主频(由于功耗和发热的限制,单纯提高主频难以持续提升性能)等等

多核cpu的出现

特点:同一个处理器多个核心

性能提升方式:让多个任务同时运行,从而提升整体性能

多核导致的问题

  • 缓存不一致问题

    如果各个核心的缓存数据不同步,就会导致数据不一致问题

MESI

缓存一致性协议,解决多核下缓存不一致的问题,最常见的缓存一致性方案

  • Modified (M) :缓存中的数据被修改,且与主内存中的数据不同。
  • Exclusive (E) :缓存中的数据与主内存相同,且只有该核心拥有此数据。
  • Shared (S) :多个核心的缓存中都可能有相同的数据。
  • Invalid (I) :缓存中的数据无效,不能使用。

工作机制:MESI 通过监控各个核心之间的数据状态(嗅探机制),保证每当一个核心修改了数据,其他核心的缓存会自动将该数据标记为无效,从而保证一致性。

这里说明一下,MESI是一种协议,所以它会有各种实现,甚至嗅探机制也有各种实现(存在延迟可能)

MESI的优化:

  • 传统嗅探机制,要求各核心立即响应其他核心的读写操作,性能太低,此时CPU引入了Invalid Queue

Invalid Queue

缓存无效化操作:当 MESI 协议检测到某个核心修改了数据,它会通知其他核心无效化相关缓存行。

解决方案是把同步改为异步,增加Invalid Queue异步处理,可以批量或延迟处理无效化操作,减少总线带宽压力

带来的问题:主要问题是可能导致的延迟同步

Store Buffer

为了提高写操作效率,CPU 引入了 Store Buffer,允许写操作先暂存到缓冲区,而不是直接写入缓存或内存。直到缓存满了、空闲等时候真正刷到内存(常规套路了)

带来的问题:由于 Store Buffer 的存在,当一个核心修改数据并将其暂存到 Store Buffer 中时,MESI 可能还未检测到这一变化。这就带来了可见性问题

Invalid Queue和store buffer的使用类似于消息队列,导致的问题是MESI更加容易出现数据不一致

如何解决Invalid Queue和store buffer造成的问题?

为了解决这两个带来的可见性问题,处理器设计了 内存屏障给程序员使用。

内存屏障

写屏障 (Store Barrier) :确保写操作真正写回到内存,清空 Store Buffer。保证其他核心读取到最新数据。

读屏障 (Load Barrier) :确保在屏障前的读操作完成,确保 Invalid Queue 中的无效化操作已处理

全屏障 (Full Barrier) :结合读写屏障的功能,确保读写操作的完整顺序,清空 Store Buffer 并处理 Invalid Queue,确保多核环境中的数据一致性和可见性。

通过这些机制,内存屏障在多核处理器中协调 Store BufferInvalid Queue,解决了由于指令重排、缓存滞留等带来的数据不一致问题。

高级语言如何使用内存屏障

Java

  • volatile关键字

    在 Java 中,volatile 关键字用于声明一个共享变量

    写入 volatile 变量:会在写入时插入一个 写屏障,确保之前对普通变量的写操作都已经完成并刷新到内存。

    读取 volatile 变量:会在读取时插入一个 读屏障,确保之后的读操作读取到的是主内存中的最新值。

  • synchronized 关键字

    synchronized 块或方法会确保进入块或者方法之前,其他线程对共享变量的修改对当前线程可见,这相当于在进入和退出时分别插入了内存屏障。