[转帖]什么是内存屏障? Why Memory Barriers ?
要了解如何使用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中。如果按照下面流程执行这段代码:
- CPU 0 starts executing the a = 1
- CPU 0 looks “a” up in the cache, and finds that it is missing
- CPU 0 therefore sends a “read invalidate” message in order to get exclusive ownership of the cache line containing “a”.
- CPU 0 records the store to “a” in its store buffer.
- CPU 1 receives the “read invalidate” message, and responds by transmitting the cache line and removing that cacheline from its cache.
- CPU 0 starts executing the b = a + 1.
- CPU 0 receives the cache line from CPU 1, which still has a value of zero for “a”.
- CPU 0 loads “a” from its cache, finding the value zero.
- 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.
- 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).
- 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()。
程序执行的顺序如下:
- CPU 0 执行a = 1。缓存行不在CPU0的缓存中,因此CPU0将“a”的新值放到存储缓冲区,并发送一个“读使无效”消息
- CPU 1 执行while (b == 0) continue,但是包含“b”的缓存行不在缓存中,它发送一个“读”消息。
- CPU 0 执行b = 1,它已经在缓存行中有“b”的值了(换句话说,缓存行已经处于“modified”或者“exclusive”状态),因此它存储新的“b”值在它的缓存行中。
- CPU 0 接收到“读”消息,并且发送缓存行中的最新的“b”的值到CPU1,同时将缓存行设置为“shared”状态
- CPU 1 接收到包含“b”值的缓存行,并将其值写到它的缓存行中
- CPU 1 现在结束执行while (b == 0) continue,因为它发现“b”的值是1,它开始处理下一条语句。
- CPU 1 执行assert(a == 1),并且,由于CPU 1 工作在旧的“a”的值,因此验证失败。
- CPU 1 接收到“读使无效”消息, 并且发送包含“a”的缓存行到CPU0,同时使它的缓存行变成无效。但是已经太迟了。
- 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);
-
}
- CPU0执行a=1。因为cache-line是shared状态,所以新值放到store-buffer里,并传递invalidate消息来通知CPU1
- CPU1执行 while(b==0) continue;但是b不再CPU1-cache中,所以发送read消息
- CPU1接受到CPU0的invalidate消息,将其排队,然后返回ACK消息
- CPU0接收到来自CPU1的ACK消息,然后执行smp_mb(),将a从store-buffer移到cache-line中。(内存屏蔽在此处生效了)
- CPU0执行b=1;因为已经包含了该cache-line,所以将b的新值写入cache-line
- CPU0接收到了read消息,于是传递包含b新值的cache-line给CPU1,并标记为shared状态
- CPU1接收到包含b的cache-line
- CPU1继续执行while(b==0) continue;因为为假所以进行下一个语句
- CPU1执行assert(a==1),因为a的旧值依然在CPU1-cache中,断言失败
- 尽管断言失败了,但是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);
-
}
[转帖]什么是内存屏障? Why Memory Barriers ?的更多相关文章
- 内存屏障(Memory barrier)-- 转发
本文例子均在 Linux(g++)下验证通过,CPU 为 X86-64 处理器架构.所有罗列的 Linux 内核代码也均在(或只在)X86-64 下有效. 本文首先通过范例(以及内核代码)来解释 Me ...
- JVM内存模型、指令重排、内存屏障概念解析
在高并发模型中,无是面对物理机SMP系统模型,还是面对像JVM的虚拟机多线程并发内存模型,指令重排(编译器.运行时)和内存屏障都是非常重要的概念,因此,搞清楚这些概念和原理很重要.否则,你很难搞清楚哪 ...
- [面试]volatile类型修饰符/内存屏障/处理器缓存
volatile类型修饰符 本篇文章的目的是为了自己梳理面试知识点, 在这里做一下笔记. 绝大部分内容是基于这些文章的内容进行了copy+整理: 1. http://www.infoq.com/cn/ ...
- JVM内存模型、指令重排、内存屏障概念解析(转载)
在高并发模型中,无是面对物理机SMP系统模型,还是面对像JVM的虚拟机多线程并发内存模型,指令重排(编译器.运行时)和内存屏障都是非常重要的概念,因此,搞清楚这些概念和原理很重要.否则,你很难搞清楚哪 ...
- C和C++中的volatile、内存屏障和CPU缓存一致性协议MESI
目录 1. 前言2 2. 结论2 3. volatile应用场景3 4. 内存屏障(Memory Barrier)4 5. setjmp和longjmp4 1) 结果1(非优化编译:g++ -g -o ...
- CPU缓存和内存屏障
CPU性能优化手段 - 缓存 为了提高程序的运行性能, 现代CPU在很多方面对程序进行了优化例如: CPU高速缓存, 尽可能的避免处理器访问主内存的时间开销, 处理器大多会利用缓存以提高性能 多级缓存 ...
- volatile关键字?MESI协议?指令重排?内存屏障?这都是啥玩意
一.摘要 三级缓存,MESI缓存一致性协议,指令重排,内存屏障,JMM,volatile.单拿一个出来,想必大家对这些概念应该有一定了解.但是这些东西有什么必然的联系,或者他们之间究竟有什么前世今生想 ...
- 多线程 - 内存屏障和cpu缓存
CPU性能优化 - 缓存 为了提高程序运行的性能,现代CPU在很多方面会对程序进行优化.CPU的处理速度是很快的,内存的速度次之,硬盘速度最慢.在cpu处理内存数据中,内存运行速度太慢,就会拖累cpu ...
- Java 内存屏障
内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题.Java编译器也会根据内存屏障的规则禁止重排序. 内 ...
- [SPDK/NVMe存储技术分析]006 - 内存屏障(MB)
在多核(SMP)多线程的情况下,如果不知道CPU乱序执行的话,将会是一场噩梦,因为无论怎么进行代码Review也不可能发现跟内存屏障(MB)相关的Bug.内存屏障分为两类: 跟编译有关的内存屏障: 告 ...
随机推荐
- 【笔记-错误】springCloud-alibaba-feign集成sentinel的启动报错
背景 随着Spring Cloud Alibaba 2.2.0.RELEASE的发布,终于可以使用最新的Spring Boot和Spring Cloud. 现在的环境 依赖 版本 Spring Boo ...
- 在macOS中搭建.NET MAUI开发环境
@ 目录 准备 安装扩展 安装 .NET 安装工作负载 安装 Xcode 命令行工具 调试安卓应用 安装 JDK 安装 Android SDK 安装 Android 模拟器 安装模拟器 安装镜像 创建 ...
- JVM优化:如何进行JVM调优,JVM调优参数有哪些
Java虚拟机(JVM)是Java应用运行的核心环境.JVM的性能优化对于提高应用性能.减少资源消耗和提升系统稳定性至关重要.本文将深入探讨JVM的调优方法和相关参数,以帮助开发者和系统管理员有效地优 ...
- 2021-01-20:mysql中,一张表里有3亿数据,未分表,要求是在这个大表里添加一列数据。数据库不能停,并且还有增删改操作。请问如何操作?
2021-01-20:mysql中,一张表里有3亿数据,未分表,要求是在这个大表里添加一列数据.数据库不能停,并且还有增删改操作.请问如何操作? 福哥答案2020-01-20: 陌陌答案: 用pt_o ...
- vue星星评分
<template> <div> <div class="xx"> <div v-for="(i, index) in list ...
- 如何利用动态配置中心在JavaAgent中实现微服务的多样化治理
本文分享自华为云社区<如何利用动态配置中心在JavaAgent中实现微服务的多样化治理>,作者:华为云开源 . 一.前言 随着JavaAgent在微服务治理方面的广泛应用和发展,我们可以在 ...
- 云小课|MRS基础原理之Hue组件介绍
阅识风云是华为云信息大咖,擅长将复杂信息多元化呈现,其出品的一张图(云图说).深入浅出的博文(云小课)或短视频(云视厅)总有一款能让您快速上手华为云.更多精彩内容请单击此处. 摘要:Hue是一组WEB ...
- 音视频传输协议众多, 5G时代不同业务应该如何选择?
摘要:音视频传输协议众多, 不同业务应该如何选择? RTSP.RTMP.RTP/RTC.HLS.MSS.DASH.WEBRTC.RIST.SRT:在此我们就从业务发展的视角来理解各种流媒体协议,帮助大 ...
- 3层结构+7大特点,带你认识华为云IoTEdge
摘要:华为云IoTEdge三层架构联合打造的IoT边缘平台功能,持续为更多企业和开发者带来价值. 本文分享自华为云社区<[华为云IoTEdge学习笔记]华为云IoT边缘功能特点及价值>,作 ...
- 测试用例又双叒叕失败了,NLP帮你
摘要:本文将介绍如何使用AI技术实现失败测试用例的智能分析. 本文分享自华为云社区<测试用例又双叒叕失败了,啥原因?NLP帮你来分析>,作者: 敏捷的小智 . 随着软件行业的快速发展,为了 ...