Why Memory Barrier?
引言:
xchg做了什么?
首先,xchg eax, ecx并不会比mov edx, eax + mov eax, ecx + mov ecx, edx这三条指令加一起快,原因是xchg有副作用。
Microsoft Platform SDK:
MemoryBarrierThe MemoryBarrier macro ensures that all memory load or store operations before it are completed before any load or store operations following it.void MemoryBarrier(void);ParametersThis macro has no parameters. Return ValuesThis macro has no return values. RemarksThis macro is called when the order of memory reads and writes are critical for program operation. It is often used with multithread synchronization functions such as InterlockedExchange.This macro can be called on all processor platforms where Windows is supported, but it has no effect on some platforms. The definition varies from platform to platform. The following are some definitions of this macro in Winnt.h:#define MemoryBarrier _mm_mfence FORCEINLINEVOIDMemoryBarrier ( VOID ){ LONG Barrier; __asm { xchg Barrier, eax }}
#define MemoryBarrier __mf
也就是说xchg指令除了交换两个操作数之外,还导致CPU的硬件缓存被刷新。
要了解如何使用memory barrier,最好的方法是明白它为什么存在。CPU硬件设计为了提高指令的执行速度,增设了两个缓冲区(store buffer, invalidate queue)。这个两个缓冲区可以避免CPU在某些情况下进行不必要的等待,从而提高速度,但是这两个缓冲区的存在也同时带来了新的问题。
要仔细分析这个问题需要先了解cache的工作方式。
目前CPU的cache的工作方式很像软件编程所使用的hash表,书上说“N路组相联(N-way set associative)”,其中的“组”就是hash表的模值,即hash链的个数,而常说的“N路”,就是每个链表的最大长度。链表的表项叫做 cache-line,是一段固定大小的内存块。读操作很直接,不再赘述。如果某个CPU要写数据项,必须先将该数据项从其他CPU的cache中移出, 这个操作叫做invalidation。当invalidation结束,CPU就可以安全的修改数据了。如果数据项在该CPU的cache中,但是是只 读的,这个过程叫做”write miss”。一旦CPU将数据从其他CPU的cache中移除,它就可以重复的读写该数据项了。如果此时其他CPU试图访问这个数据项,将产生一 次”cache miss”,这是因为第一个CPU已经使数据项无效了。这种类型的cache-miss叫做”communication miss”,因为产生这种miss的数据项通常是做在CPU之间沟通之用,比如锁就是这样一种数据项。
为了保证在多处理器的环境下cache仍然一致,需要一种协议来防止数据不一致和丢失。目前常用的协议是MESI协议。MESI是 Modified,Exclusive, Shared, Invalid这四种状态的首字母的组合。使用该协议的cache,会在每个cache-line前加一个2位的tag,标示当前的状态。
modified状态:该cache-line包含修改过的数据,内存中的数据不会出现在其他CPU-cache中,此时该CPU的cache中包含的数据是最新的
exclusive状态:与modified类似,但是数据没有修改,表示内存中的数据是最新的。如果此时要从cache中剔除数据项,不需要将数据写回内存
shared状态:数据项可能在其他CPU中有重复,CPU必须在查询了其他CPU之后才可以向该cache-line写数据
invalid状态:表示该cache-line空
MESI使用消息传递的方式在上述几种状态之间切换,具体转换过程参见[1]。如果CPU使用共享BUS,下面的消息足够:
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写回内存,为其他数据腾出空间
引用[1]中的话:
Interestingly enough, a shared-memory multiprocessor system really is a message-passing computer under the covers. This means that clusters of SMP machines that use distributed shared memory are using message passing to implement shared memory at two different levels of the system architecture.
虽然该协议可以保证数据的一致性,但是在某种情况下并不高效。举例来说,如果CPU0要更新一个处于CPU1-cache中的数据,那么它必须等待 cache-line从CPU1-cache传递到CPU0-cache,然后再执行写操作。cache之间的传递需要花费大量的时间,比执行一个简单的 操作寄存器的指令高出几个数量级。而事实上,花费这个时间根本毫无意义,因为不论从CPU1-cache传递过来的数据是什么,CPU0都会覆盖它。为了 解决这个问题,硬件设计者引入了store buffer,该缓冲区位于CPU和cache之间,当进行写操作时,CPU直接将数据写入store buffer,而不再等待另一个CPU的消息。但是这个设计会导致一个很明显的错误情况。
试考虑如下代码:
1 a = 1;
2 b = a + 1;
3 assert(b == 2);
假设初始时a和b的值都是0,a处于CPU1-cache中,b处于CPU0-cache中。如果按照下面流程执行这段代码:
1 CPU0执行a=1;
2 因为a在CPU1-cache中,所以CPU0发送一个read invalidate消息来占有数据
3 CPU0将a存入store buffer
4 CPU1接收到read invalidate消息,于是它传递cache-line,并从自己的cache中移出该cache-line
5 CPU0开始执行b=a+1;
6 CPU0接收到了CPU1传递来的cache-line,即“a=0”
7 CPU0从cache中读取a的值,即“0”
8 CPU0更新cache-line,将store buffer中的数据写入,即“a=1”
9 CPU0使用读取到的a的值“0”,执行加1操作,并将结果“1”写入b(b在CPU0-cache中,所以直接进行)
10 CPU0执行assert(b == 2); 失败
出现问题的原因是我们有两份”a”的拷贝,一份在cache-line中,一份在store buffer中。硬件设计师的解决办法是“store forwarding”,当执行load操作时,会同时从cache和store buffer里读取。也就是说,当进行一次load操作,如果store-buffer里有该数据,则CPU会从store-buffer里直接取出数 据,而不经过cache。因为“store forwarding”是硬件实现,我们并不需要太关心。
还有一中错误情况,考虑下面的代码:
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 CPU0执行 a = 1; 因为a不在CPU0-cache中,所以CPU0将a的值放到store-buffer里,然后发送read invalidate消息
2 CPU1执行while(b == 0) continue; 但是因为b不再CPU1-cache中,所以它会发送一个read消息
3 CPU0执行 b = 1;因为b在CPU0-cache中,所以直接存储b的值到store-buffer中
4 CPU0收到 read 消息,于是它将更新过的b的cache-line传递给CPU1,并标记为shared
5 CPU1接收到包含b的cache-line,并安装到自己的cache中
6 CPU1现在可以继续执行while(b == 0) continue;了,因为b=1所以循环结束
7 CPU1执行assert(a == 1);因为a本来就在CPU1-cache中,而且值为0,所以断言为假
8 CPU1收到read invalidate消息,将并将包含a的cache-line传递给CPU0,然后标记cache-line为invalid。但是已经太晚了
就是说,可能出现这类情况,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”。具体过程见[1]。
上面的例子是使用memory barrier的一种环境,另一种环境涉及到另一个缓冲区,确切的说是一个队列——“Invalidate Queues”。
store buffer一般很小,所以CPU执行几个store操作就会填满。这时候CPU必须等待invalidation ACK消息,来释放缓冲区空间——得到invalidation ACK消息的记录会同步到cache中,并从store buffer中移除。同样的情形发生在memory barrier执行以后,这时候所有后续的store操作都必须等待invalidation完成,不论这些操作是否导致cache-miss。解决办法 很简单,即使用“Invalidate Queues”将invalidate消息排队,然后马上返回invalidate ACK消息。不过这种方法有问题。
考虑下面的情况:
void foo(void)
{
a = 1;
smp_mb();
b = 1;
}
void bar(void)
{
while (b == 0) continue;
assert(a == 1);
}
a处于shared状态,b在CPU0-cache内。CPU0执行foo(),CPU1执行函数bar()。执行操作如下:
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消息后,在它还没有处理这个消息之前,就再次读取该消息对应的数据了,该数据此时本应该已经失效的。
解决方法是在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中读取。
memory bariier还可以细分为“write memory barrier(wmb)”和“read memory barrier(rmb)”。rmb只处理Invalidate Queues,wmb只处理store buffer。
可以使用rmb和wmb重写上面的例子:
void foo(void)
{
a = 1;
smp_wmb();
b = 1;
}
void bar(void)
{
while (b == 0) continue;
smp_rmb();
assert(a == 1);
}
最后提一下x86的mb。x86CPU会自动处理store顺序,所以smp_wmb()原语什么也不做,但是load有可能乱序,smp_rmb()和smp_mb()展开为lock;addl。
[1] http://www.rdrop.com/users/paulmck/scalability/paper/whymb.2010.06.07c.pdf
[2] http://en.wikipedia.org/wiki/Memory_barrier
[3] http://www.mjmwired.net/kernel/Documentation/memory-barriers.txt
Why Memory Barrier?的更多相关文章
- ARM有几条memory barrier 的指令?分别有什么区别?
从ARMv7指令集开始,ARM提供3条内存屏障指令. (1)数据存储屏障( Data Memory Barrier,DMB) 数据存储器隔离.DMB指令保证:仅当所有在它前面的存储器访问操作都执行完毕 ...
- 内存屏障 & Memory barrier
Memory Barrier http://www.wowotech.net/kernel_synchronization/memory-barrier.html 这里面讲了Memory Barrie ...
- 理解 Memory barrier
理解 Memory barrier(内存屏障) 发布于 2014 年 04 月 21 日2014 年 05 月 15 日 作者 name5566 参考文献列表:http://en.wikipedia. ...
- Linux内核同步机制之(三):memory barrier【转】
转自:http://www.wowotech.net/kernel_synchronization/memory-barrier.html 一.前言 我记得以前上学的时候大家经常说的一个词汇叫做所见即 ...
- 内存栅栏(memory barrier):解救peterson算法的应用陷阱
最近一个项目中用到了peterson算法来做临界区的保护,简简单单的十几行代码,就能实现两个线程对临界区的无锁访问,确实很精炼.但是在这不是来分析peterson算法的,在实际应用中发现peterso ...
- Linux内核同步 - memory barrier
一.前言 我记得以前上学的时候大家经常说的一个词汇叫做所见即所得,有些编程工具是所见即所得的,给程序员带来极大的方便.对于一个c程序员,我们的编写的代码能所见即所得吗?我们看到的c程序的逻辑是否就是最 ...
- 内存屏障(Memory barrier)-- 转发
本文例子均在 Linux(g++)下验证通过,CPU 为 X86-64 处理器架构.所有罗列的 Linux 内核代码也均在(或只在)X86-64 下有效. 本文首先通过范例(以及内核代码)来解释 Me ...
- 并行计算之Memory barrier(内存
本文转载自:http://name5566.com/4535.html 参考文献列表:http://en.wikipedia.org/wiki/Memory_barrierhttp://en.wiki ...
- 理解 Memory barrier(内存屏障)无锁环形队列
原文:https://www.cnblogs.com/my_life/articles/5220172.html Memory barrier 简介 程序在运行时内存实际的访问顺序和程序代码编写的访问 ...
随机推荐
- Java---设计模块(设计模块的简介及最简单的俩个单例代码加测试)
设计模式学习概述: ★ 为什么要学习设计模式 1.设计模式都是一些相对优秀的解决方案,很多问题都是典型的.有代表性的问题,学习设计模式,我们就不用自己从头来解决这些问题,相当于在巨人的肩膀上,复用这些 ...
- [Operationg System Labs] 我对 Linux0.00 中 boot.s的理解和注释
(如有错误请立即指正,么么哒!) ! boot.s!! It then loads the system at 0x10000, using BIOS interrupts. Thereafte ...
- HDU_1426——数独问题,DFS
Problem Description 自从2006年3月10日至11日的首届数独世界锦标赛以后,数独这项游戏越来越受到人们的喜爱和重视. 据说,在2008北京奥运会上,会将数独列为一个单独的项目进行 ...
- HDU_2034——集合A-B
Problem Description 参加过上个月月赛的同学一定还记得其中的一个最简单的题目,就是{A}+{B},那个题目求的是两个集合的并集,今天我们这个A-B求的是两个集合的差,就是做集合的减法 ...
- HDU_2011——求多项式的前n项和
Problem Description 多项式的描述如下:1 - 1/2 + 1/3 - 1/4 + 1/5 - 1/6 + ...现在请你求出该多项式的前n项的和. Input 输入数据由2行组 ...
- guestmount
guestmountFor some types of changes, you may find it easier to mount the image's file system directl ...
- UVa 116: Undirectional TSP
简单动态规划题.用取模实现第一行与最后一行连续,注意取字典序即可. 我的解题代码如下: #include <iostream> #include <cstdio> #inclu ...
- Highcharts教程
Highcharts特性: 兼容性 - 支持所有主流浏览器和移动平台(android.iOS等). 多设备 - 支持多种设备,如手持设备 iPhone/iPad.平板等 免费使用 - 开源免费 轻量 ...
- Agile 敏捷开发
简单的说,敏捷开发是一种以人为核心.迭代.循序渐进的开发方法.在敏捷开发中,软件项目的构建被切分成多个子项目,各个子项目的成果都经过测试,具备集成和可运行的特征.换言之,就是把一个大项目分为多个相互联 ...
- 论js闭包的重要性
很久没写博客了,今天发现了一个很有意思的问题,写下来分享一下 话不多说,贴前端代码: <script type="text/javascript" src="js/ ...