要了解如何使用memory barrier,最好的方法是明白它为什么存在。CPU硬件设计为了提高指令的执行速度,增设了两个缓冲区(store buffer, invalidate queue)。这个两个缓冲区可以避免CPU在某些情况下进行不必要的等待,从而提高速度,但是这两个缓冲区的存在也同时带来了新的问题。下面我们一步步来分析说明

1. cache一致性问题

Cache 一致性问题出现的原因是在一个多处理器系统中,每个处理器核心都有独占的Cache 系统(比如一级 Cache 和二级 Cache),而导致一个内存块在系统中同时可能有多个备份,从而引起访问时的不一致性问题。

Cache 一致性问题的根源是因为存在多个处理器独占的 Cache,而不是多个处理器。它的限制条件比较多:多核,独占 Cache,Cache 写策略。当其中任一个条件不满足时便不存在cache一致性问题。

为了保证在多处理器的环境下cache一致性,需要通过某种手段来保证cache的一致性。

解决 Cache 一致性问题的机制有两种:基于目录的协议(Directory-based protocol)和总线窥探协议(Bus snooping protocol)。 目前被多个厂家广泛使用的协议是MESI协议。它是从总线窥探协议中衍生而来的一种协议。

2. cache一致性协议:MESI

MESI 协议是 Cache line 四种状态的首字母的缩写,分别是修改(Modified)态、独占(Exclusive)态、共享(Shared)态和失效(Invalid)态。 Cache 中缓存的每个 Cache Line 都必须是这四种状态中的一种。

  • 修改态(Modified),如果该 Cache Line 在多个 Cache 中都有备份,那么只有一个备份能处于这种状态,并且“dirty”标志位被置上。拥有修改态 Cache Line 的 Cache 需要在某个合适的时候把该 Cache Line 写回到内存中。但是在写回之前,任何处理器对该 Cache Line在内存中相对应的内存块都不能进行读操作。 Cache Line 被写回到内存中之后,其状态就由修改态变为共享态。
  • 独占态(Exclusive),和修改状态一样,如果该 Cache Line 在多个 Cache 中都有备份,那么只有一个备份能处于这种状态,但是“dirty”标志位没有置上,因为它是和主内存内容保持一致的一份拷贝。如果产生一个读请求,它就可以在任何时候变成共享态。相应地,如果产生了一个写请求,它就可以在任何时候变成修改态。
  • 共享态(Shared),意味着该 Cache Line 可能在多个 Cache 中都有备份,并且是相同的状态,它是和内存内容保持一致的一份拷贝,而且可以在任何时候都变成其他三种状态。
  • 失效态(Invalid),该 Cache Line 要么已经不在 Cache 中,要么它的内容已经过时。一旦某个Cache Line 被标记为失效,那它就被当作从来没被加载到 Cache 中

MESI使用消息传递的方式在上述几种状态之间切换,具体转换过程参见[1]。常见的消息类型:

read: 包含要读取的CACHE-LINE的物理地址
read response: 包含READ请求的数据,要么由内存满足要么由cache满足
invalidate: 包含要invalidate的cache-line的物理地址,所有其他cache必须移除相应的数据项
invalidate ack: 回复消息
read invalidate: 包含要读取的cache-line的物理地址,同时使其他cache移除该数据。需要read response和invalidate ack消息
writeback:包含要写回的数据和地址,该状态将处于modified状态的lines写回内存,为其他数据腾出空间

3. Store buffer的引入

虽然该MESI协议可以保证数据的一致性,但是在某种情况下并不高效。举例来说,如果CPU0要更新一个处于CPU1-cache中的数据,那么它必须等待 cache-line从CPU1-cache传递到CPU0-cache,然后再执行写操作。cache之间的传递需要花费大量的时间,比执行一个简单的 操作寄存器的指令高出几个数量级。而事实上,花费这个时间根本毫无意义,因为不论从CPU1-cache传递过来的数据是什么,CPU0都会覆盖它。为了 解决这个问题,硬件设计者引入了store buffer,该缓冲区位于CPU和cache之间,当进行写操作时,CPU直接将数据写入store buffer,而不再等待另一个CPU的消息。但是这个设计会导致一个很明显的错误情况。

3.1 引入store buffer后出现的问题1

试考虑如下代码:

a = 1;
b = a + 1;
assert(b == 2);

假设初始时a和b的值都是0,a处于CPU1-cache中,b处于CPU0-cache中。如果按照下面流程执行这段代码:

  1. CPU 0 starts executing the a = 1
  2. CPU 0 looks “a” up in the cache, and finds that it is missing
  3. CPU 0 therefore sends a “read invalidate” message in order to get exclusive ownership of the cache line containing “a”.
  4. CPU 0 records the store to “a” in its store buffer.
  5. CPU 1 receives the “read invalidate” message, and responds by transmitting the cache line and removing that cacheline from its cache.
  6. CPU 0 starts executing the b = a + 1.
  7. CPU 0 receives the cache line from CPU 1, which still has a value of zero for “a”.
  8. CPU 0 loads “a” from its cache, finding the value zero.
  9. CPU 0 applies the entry from its store buffer to the newly arrived cache line, setting the value of “a” in its cache to one.
  10. CPU 0 adds one to the value zero loaded for “a” above, and stores it into the cache line containing “b” (which we will assume is already owned by CPU 0).
  11. CPU 0 executes assert(b == 2), which fails.

出现问题的原因是我们有两份”a”的拷贝,一份在cache-line中,一份在store buffer中。硬件设计师的解决办法是“store forwarding”,当执行load操作时,会同时从cache和store buffer里读取。也就是说,当进行一次load操作,如果store-buffer里有该数据,则CPU会从store-buffer里直接取出数 据,而不经过cache。因为“store forwarding”是硬件实现,我们并不需要太关心。

3.2 引入store buffer后出现的问题2

请看下面的代码:

void foo(void)
{a = 1;b = 1;
}void bar(void)
{while (b == 0) continue;assert(a == 1);
}

假设变量a在CPU1-cache中,b在CPU0-cache中。CPU0执行foo(),CPU1执行bar()。

程序执行的顺序如下:

  1. CPU 0 执行a = 1。缓存行不在CPU0的缓存中,因此CPU0将“a”的新值放到存储缓冲区,并发送一个“读使无效”消息
  2. CPU 1 执行while (b == 0) continue,但是包含“b”的缓存行不在缓存中,它发送一个“读”消息。
  3. CPU 0 执行b = 1,它已经在缓存行中有“b”的值了(换句话说,缓存行已经处于“modified”或者“exclusive”状态),因此它存储新的“b”值在它的缓存行中。
  4. CPU 0 接收到“读”消息,并且发送缓存行中的最新的“b”的值到CPU1,同时将缓存行设置为“shared”状态
  5. CPU 1 接收到包含“b”值的缓存行,并将其值写到它的缓存行中
  6. CPU 1 现在结束执行while (b == 0) continue,因为它发现“b”的值是1,它开始处理下一条语句。
  7. CPU 1 执行assert(a == 1),并且,由于CPU 1 工作在旧的“a”的值,因此验证失败。
  8. CPU 1 接收到“读使无效”消息, 并且发送包含“a”的缓存行到CPU0,同时使它的缓存行变成无效。但是已经太迟了。
  9. CPU 0 接收到包含“a”的缓存行,将且将存储缓冲区的数据保存到缓存行中,这使得CPU1验证失败。

就是说,可能出现这类情况,b已经赋值了,但是a还没有,所以出现了b = 1, a = 0的情况。对于这类问题,硬件设计者也爱莫能助,因为CPU无法知道变量之间的关联关系。所以硬件设计者提供了memory barrier指令,让软件来告诉CPU这类关系。

解决办法是:使用硬件设计者提供的“内存屏障”来修改代码:

void foo(void)
{a = 1;smp_mb();b = 1;
}

smp_mb()指令可以迫使CPU在进行后续store操作前刷新store-buffer。以上面的程序为例,增加memory barrier之后,就可以保证在执行b=1的时候CPU0-store-buffer中的a已经刷新到cache中了,此时CPU1-cache中的a 必然已经标记为invalid。对于CPU1中执行的代码,则可以保证当b==0为假时,a已经不在CPU1-cache中,从而必须从CPU0- cache传递,得到新值“1”

4. Invalidation Queue的引入

store buffer一般很小,所以CPU执行几个store操作就会填满, 这时候CPU必须等待invalidation ACK消息(得到invalidation ACK消息后会将storebuffer中的数据存储到cache中,然后将其从store buffer中移除),来释放store buffer缓冲区空间。

Invalidation ACK消息需要如此长的时间,其原因之一是它们必须确保相应的缓存行实际变成无效了。如果缓存比较忙的话,这个使无效操作可能被延迟。例如,如果CPU密集的装载或者存储数据,并且这些数据都在缓存中。另外,如果在一个较短的时间内,大量的“使无效”消息到达,一个特定的CPU会忙于处理它们。这会使得其他CPU陷于停顿。但是,在发送应答前,CPU 不必真正的使无效缓存行。它可以将使无效消息排队。并且它明白,在发送更多的关于该缓存行的消息前,需要处理这个消息。

一个带Invalidation Queue的CPU可以迅速应答一个Invalidation Ack消息,而不必等待相应的行真正变成无效状态。于是乎出现了下面的组织架构:

但是,此种方法也存在如下问题:

void foo(void)
{a = 1;smp_mb();b = 1;
}void bar(void)
{while (b == 0) continue;assert(a == 1);
}
  1. CPU0执行a=1。因为cache-line是shared状态,所以新值放到store-buffer里,并传递invalidate消息来通知CPU1
  2. CPU1执行 while(b==0) continue;但是b不再CPU1-cache中,所以发送read消息
  3. CPU1接受到CPU0的invalidate消息,将其排队,然后返回ACK消息
  4. CPU0接收到来自CPU1的ACK消息,然后执行smp_mb(),将a从store-buffer移到cache-line中。(内存屏蔽在此处生效了
  5. CPU0执行b=1;因为已经包含了该cache-line,所以将b的新值写入cache-line
  6. CPU0接收到了read消息,于是传递包含b新值的cache-line给CPU1,并标记为shared状态
  7. CPU1接收到包含b的cache-line
  8. CPU1继续执行while(b==0) continue;因为为假所以进行下一个语句
  9. CPU1执行assert(a==1),因为a的旧值依然在CPU1-cache中,断言失败
  10. 尽管断言失败了,但是CPU1还是处理了队列中的invalidate消息,并真的invalidate了包含a的cache-line,但是为时已晚

出现问题的原因是,当CPU排队某个invalidate消息后,并做错了应答Invalidate Ack, 但是在它还没有处理这个消息之前,就再次读取了位于cache中的数据,该数据此时本应该已经失效,但由于未处理invalidate消息导致使用错误。

解决方法是在bar()中也增加一个memory barrier:

void bar(void)
{while (b == 0) continue;smp_mb();assert(a == 1);
}

此处smp_mb()的作用是处理“Invalidate Queues”中的消息,于是在执行assert(a==1)时,CPU1中的包含a的cache-line已经无效了,新的值要重新从CPU0-cache中读取

从这两个例子可以看出:

smp_mb();既可以用来处理storebuffer中的数据,也可以用来处理Invalidation Queue中的Invalid消息。实际上,memory barrier确实可以细分为“write memory barrier(wmb)”和“read memory barrier(rmb)”。rmb只处理Invalidate Queues,wmb只处理store buffer。

上述例子完整的代码变为:

void foo(void)
{a = 1;smp_wmb();/*CPU1要使用该值,因此需要及时更新处理store buffer中的数据*/b = 1;
}void bar(void)
{while (b == 0) continue;smp_rmb();/*由于CPU0修改了a值,使用此值时及时处理Invalidation Queue中的消息*/assert(a == 1);
}

参考文献:

【1】Why Memory Barriers ?

【2】深入理解并行编程V2.0

【3】Perf:Is Parallel Programming Hard, And, If So, What Can You Do About It?

如果有需要这两本书的大佬(【2】为【3】的中文译版),我可以提供哦!!!评论后上写明邮箱地址,我会发送给U。

什么是内存屏障? Why Memory Barriers ?相关推荐

  1. 内存屏障(Memory Barrier)与volatile

    1. 写缓存(Store Buffer) 写缓存是一个容量极小的高速存储部件,每个核都有自己的写缓存,而且一个核不能够读取到其他核的写缓存(Store Buffer)的内容. 如上面的场景,核A修改共 ...

  2. 内存屏障(Memory Barrier)(二)什么是读屏障?

    <内存屏障(Memory Barrier)(一)什么是写屏障?> <内存屏障(Memory Barrier)(二)什么是读屏障?> <内存屏障(Memory Barrie ...

  3. 全网最硬核 Java 新内存模型解析与实验 - 3. 硬核理解内存屏障(CPU+编译器)

    个人创作公约:本人声明创作的所有文章皆为自己原创,如果有参考任何文章的地方,会标注出来,如果有疏漏,欢迎大家批判.如果大家发现网上有抄袭本文章的,欢迎举报,并且积极向这个 github 仓库 提交 i ...

  4. 缓存一致性MESI与内存屏障

    文章目录 1. 高速缓存 1.1. 什么是高速缓存Cache 1.2. 缓存行 2. 伪共享问题 2.1. 什么是伪共享 2.2. 解决伪共享 3. 总线锁 4. 缓存锁 5. 缓存一致性协议 5.1 ...

  5. Linux高速缓存和内存屏障

    目录 一.高速缓存与地址映射 二.MESI缓存一致性协议和伪共享 三.指令重排序和内存屏障 四.Java  volatile和final关键字 一.高速缓存与地址映射 CPU访问内存(DRAM)较慢, ...

  6. 乱序和屏障1 : 总览 及 编译器内存屏障

    文章目录 建议阅读文档 乱序的定义 屏障的定义 硬件及软件技术的变化 优化带来的问题 阻止被优化的技术 编译器内存屏障 编译器内存屏障实验代码 CPU 内存屏障 RISCV的CPU内存屏障宏 ARM的 ...

  7. Java内存模型(Java Memory Model,JMM)

    导读 本文通过对JSR133规范的解读,详细的介绍JMM的核心理论,并将开发中常用的关键字的实现原来做了详细的介绍.通过本文读者可以了解到并发的一些基本理论,并对一些同步原语有了更深层次的理解.希望读 ...

  8. JVM内存模型、指令重排、内存屏障概念解析

    在高并发模型中,无是面对物理机SMP系统模型,还是面对像JVM的虚拟机多线程并发内存模型,指令重排(编译器.运行时)和内存屏障都是非常重要的概念,因此,搞清楚这些概念和原理很重要.否则,你很难搞清楚哪 ...

  9. 原子变量、锁、内存屏障,写得非常好!

    突然想聊聊这个话题,是因为知乎上的一个问题多次出现在了我的Timeline里:请问,多个线程可以读一个变量,只有一个线程可以对这个变量进行写,到底要不要加锁?可惜的是很多高票答案语焉不详,甚至有所错漏 ...

  10. CPU缓存和内存屏障

    CPU性能优化手段-缓存 为了提高程序运行的性能,现代CPU在很多方面对程序进行了优化. 例如:CPU高速缓存.尽可能地避免处理器访问主内存的时间开销,处理器大多会利用缓存(cache)以提高性能. ...

最新文章

  1. Java 静态变量,静态方法,静态常量(java static 关键字)
  2. 坐视——做事——做势
  3. Altium Designer09解决局域网冲突问题
  4. redis的基础命令操作
  5. 【Redis】Redis 基础知识 常用命令 命令积累
  6. Active Record Query Interface 数据查询接口(界面) 看到第8节。
  7. HDU 1285 拓普排序 基本模板例题 确定比赛名次
  8. Intel 386 and AMD x86-64 Options for GCC
  9. mcgs 施耐德tm218 变频器cv3100通过modbus连接控制
  10. 51视频编辑压缩官网
  11. 漫反射贴图与镜面光贴图
  12. 【计算机网络】TCP糊涂窗口综合症
  13. ProGAN: Network Embedding via Proximity Generative Adversarial Network 论文笔记
  14. Android入门第十四篇之画图
  15. java 延时队列_Java实现简单延迟队列和分布式延迟队列
  16. Visio如何把形状置于底层
  17. 第十六讲:完美主义身心健康 第十七讲:身心健康:运动与冥想 第十八讲:睡眠,触摸和爱情的重要性
  18. 看看Pwn2Own黑客大赛有哪些新技术
  19. 认知学派用计算机来比拟人,心理学基础习题解答.doc
  20. 百度api爬虫(2)json转excel表格

热门文章

  1. Arista中国战略升级 加码布局园区网络
  2. centos7.4安装MySQL
  3. 九马画山数命运, 一身伴君不羡仙! 与代码不离不弃!
  4. 谈谈我对SEO快排现象的观察及其背后原理的分析
  5. 小脚本之windows批量修改文件后缀名
  6. 20实际最牛逼的10大算法
  7. 电脑百度打不开怎么办
  8. 可过滤多种广告的“ADM(阿呆喵)广告拦截工具
  9. OCC实战1:搭建QT+OCC+VS环境
  10. 摘录--《人间》余秀华