在现代多核处理器系统中,数据的一致性和访问效率是确保高性能计算的关键因素之一。随着技术的发展和应用需求的增长,传统的单核处理架构已经无法满足日益复杂的计算任务要求。因此,多核乃至多处理器系统的出现成为必然趋势。然而,在这样的并行计算环境中,如何保证各个核心之间的数据同步与一致成为了新的挑战。
缓存一致性问题是多核系统中最为核心的问题之一。当多个处理器核心共享同一块内存区域时,每个核心都可能拥有该内存区域的副本。如果一个核心修改了其缓存中的数据,其他核心必须能够及时感知这一变化,以保持数据的一致性。为了解决这个问题,各种缓存一致性协议应运而生。
本文将从最基本的总线嗅探技术入手,逐步深入探讨几种主流的缓存一致性协议,特别是MESI协议。我们将详细介绍这些协议的工作原理、优缺点以及对系统性能的影响。希望通过本文的讲解,读者能够对缓存一致性问题有一个全面的理解,并掌握解决这一问题的有效方法。
一、详解CPU体系结构和数据读写机制
1.CPU Cache Line是什么
每个CPU都会有自己的二级缓存,其中一级缓存分为数据缓存和指令缓存,这些缓存的数据都是从内存中读取的,而且每次都会加载一个cache line,而CPU Cache Line的物理结构大体如下图所示:
关于cache line的大小可以使用命令键入如下指令进行查看:
cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size
以笔者的服务器为例,可以看到对应的输出结果为64:
同时对应的我们给出CPU缓存与内存的体系结构图,其中按照数值减小访问速度越快,不同CPU核心都有独立的二级缓存,而三级缓存则是共享缓冲区,与物理内存空间直接打交道:
如果开发者能够很好的使用缓存技术,那么程序的性能就会很高,具体可以参照笔者之前写的这篇文章
计算机组成原理-基于计组CPU的基础知识进行代码调优:https://blog.csdn.net/shark_chili3007/article/details/123038653
2.CPU Cache和内存同步技术
(1) 写直达(Write Through)技术
写直达技术解决cache和内存同步问题的方式很简单,例如CPU1要操作变量i,先看看cache中有没有变量i,若有则直接操作cache中的值,然后立刻写回内存。 若变量i不在cache中,那么CPU就回去内存中加载这个变量到cache中进行操作,然后立刻写回内存中。 这样做的好处就是实现简单,缺点也很明显,因为每次都要将修改的数据立刻写回内存,这其中的写入开销对于需要高速运转的CPU是一种灾难:
(2) 回写技术(Write Back)
Write Back即一种延迟写技术,为了避免上一种操作频繁写入内存的资源开销而提出的一种方案,它的工作原理是将数据加载到CPU cache并修改但并不写入内存,仅仅是将数据标记为dirty,由此减少写入内存的次数,如果没有发生缓存置换,这些数据就不会被写入内存中。
举个例子,CPU cache加载data1到缓存中,并进行数次修改操作,随后cpu cache发生data10不在缓存中需要从内存中加载,又因为cpu cache空间不足,此时触发缓存置换算法便将最近最少使用且是dirty的数据写到内存中,并将data10加载到cpu cache里:
可以看出这种写法如果出现在毫秒级的断电场景可能存在数据丢失问题,又因为延迟写的原因,对应的数据加载在读未命中的情况下存在两次操作:
- 将需要置换的数据写入内存。
- 将需要加载的数据拉到cpu cache。
这里我们也补充一下几种比较常见的缓存置换算法:
- Belady异常(Belady’s Anomaly)
- 最近最少使用(Least Recently Used Algorithm)
- 先进先出(FIFO)
- 后进先出(LIFO)
二、详解CPU缓存一致性问题
1.多核心缓存修改问题
当一台计算机由多核CPU构成的时候,每个CPU都从内存里加载相同的变量i(初值为0)进行累加操作,我们试想这种情况:
- CPU-0加载内存变量i值为0。
- CPU-0自增为1准备写回内存。
- CPU-1加载内存变量i值为0。
- CPU-1自增为1准备写回内存。 两次自增得到结果1,这就是经典的缓存一致性问题:
而解决这个问题我们只要攻破以下两点问题即可:
- 写传播问题:即当前Cache中修改的值要让其他CPU知道
- 事务串行化:例如CPU1先将变量i改为100,CPU2再将基于当前变量i的值乘2。我们必须保证变量i先加100,再乘2。
2.总线嗅探(Bus Snooping)
总线嗅探是解决写传播的解决方案,举个例子,当CPU1更新Cache中变量i的值时,就会通知其他核心变量i已被修改,当其他CPU发现自己Cache中也有这个值的时候就会将CPU1中cache的结果更新到自己的cache中。 这种方式缺点很明显,CPU必须无时不刻监听变化,而且出现变化的数据自己还不一定有,这样的作法增加了总线的压力:
而且也不能保证事务串行化,如下图,CPU-0加载了变量修改了值通知其他CPU这个值有变化了。 而CPU-1也改了i的值,按照正常的逻辑CPU-2、CPU-3的值应该是先变为100在变为200。 但是CPU-3先收到CPU-2的通知先改为200再收到CPU-0的通知变为100,这就导致的数据不一致的问题,即事务串行化失败:
3.MESI协议如何解决上述问题
MESI是总线嗅探的改良版,他很好的解决了总线的带宽压力,以及很好的解决了数据一致性问题。 在介绍MESI之前,我们必须了解以下MESI是什么。
- M(Modified,已修改),MESI第一个字母M,代表着CPU当前L1 cache中某个变量i的状态被修改了,而且这个数据在其他核心中都没有。
- E(Exclusive,独占),说白了就是CPUA将数据加载自己的L1 cache时,其他核心的cache中并没有这个数据,所以CPUA将这个数据加载到自己的cache时标记为E。
- (S:Shared,共享):说明CPUA在加载这个数据时,其他CPU已经加载过这个数据了,这时CPUA就会从其他CPU中拿到这个数据并加载到L1 cache中,并且所有拥有这个值的CPU都会将cache中的这个值标记为S。
- (I:Invalidated,已失效):当CPUA修改了L1 cache中的变量i时,发现这个值是S即共享的数据,那么就需要通知其他核心这个数据被改了,其他CPU都需要将cache中的这个值标为I,后面要操作的时,必须拿到最新的数据在进行操作。
好了介绍完这几个状态之后,我们不妨用一个例子过一下这个流程:
- CPU-0要加载变量i,发现变量i不在cache中,于是去内存中加载数据,此时通过总线发个消息给其他核心,其他核心的cache中并没有这条数据,所以这个变量的cache中的状态为E(独占)。
- CPU-1也加载这个数据了,在总线上发了个消息,发现CPU-0有这个数据且并没有修改或者失效的标志,于是他们一起将这个变量i状态设置为S(共享)
- CPU-0要改变量i值了,发消息给其他核心,其他核心收到消息将自己的变量i设置为I(无效),CPU-0改完后将数据设置为M(已修改)
- CPU-0又要改变量i的值了,而且看到变量i的状态为M(已修改),说明这个值是最新的数据,所以不发消息给其他核心了,直接更新即可。
- CPU-1要加载变量i,发现状态为I,于是CPU-0将值写回内存,此时状态为E,然后CPU-1读取这个值,大家状态都变为S共享。
- CPU-0要加载新的变量i了,而且变量x要使用的cache空间正是变量i的,所以CPU-0将值写回内存中,这时候内存和最新数据同步了。
三、小结
自此,我们从多核CPU体系结构所引发缓存一致性问题为入手,再从总线嗅探到MESI协议深度讲解了缓存一致性问题的解决方案,希望对你有帮助。