揭开内存屏障的面纱

推荐阅读(强烈推荐)c++标准库内存屏障的使用

一 什么是内存屏障   

    内存屏障(英语:Memory barrier),也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作。

    每个CPU都会有自己的缓存(有的甚至L1,L2,L3),缓存的目的就是为了提高性能,避免每次都要向内存取。因此大多数现代计算机为了提高性能而采取乱序执行。但是这样的弊端也很明显:不能实时的和内存发生信息交换,分在不同CPU执行的不同线程对同一个变量的缓存值不同。

    如何解决以上的问题?那就是内存屏障,内存屏障是硬件层的概念,不同的硬件平台实现内存屏障的手段并不是一样.语义上,内存屏障之前的所有写操作都要写入内存;内存屏障之后的读操作都可以获得同步屏障之前的写操作的结果。因此,对于敏感的程序块,写操作之后、读操作之前可以插入内存屏障。 

   大多数处理器提供了内存屏障指令:

1) 完全内存屏障(full memory barrier)保障了早于屏障的内存读写操作的结果提交到内存之后,再执行晚于屏障的读写操作。
2) 内存读屏障(read memory barrier)仅确保了内存读操作;
3) 内存写屏障(write memory barrier)仅保证了内存写操作。

  很简单的一个例子

二、as-if-serial语义
     As-if-serial语义的意思是,所有的动作(Action)都可以为了优化而被重排序,但是必须保证它们重排序后的结果和程序代码本身的应有结果是一致的。

int a = 1;
int b = 2;
int c = a + b;

    对a赋值1,对b赋值2,取a的值,取b的值 ,将取到两个值相加后存入c 在上面5个动作中,在编译器编译后,由于编译器的优化或者cpu的乱序执行,动作1可能会和动作2、4 重排序,动作2可能会和动作1、3重排序,动作3可能会和动作2、4重排序,动作4可能会和1、3重排序。但动作1和动作3、5不能重排序。动作2和动作4、5不能重排序。因为它们之间存在数据依赖关系,一旦重排,as-if-serial语义便无法保证。看下面的例子

// thread 1
while (!ok);
do(x);
 
// thread 2
x = 42;    // #1
ok = 1;    // #2

    ok 初始化为 0,线程 1 等待 ok 被设置为 1 后执行 do 函数。由于#1和#2并没有所谓的数据依赖关系,假如线程2编译器对 #1和#2写重排序,那么在线程1读到下的值时,并不一定是我们期望的42。

  如何保证我们读到的x值是我们期望的呢?首先我们来看几个问题

 1)多线程编程与内存可见性
     多线程程序通常使用高层程序设计语言中的同步原语,如Java与.NET Framework,或者API如pthread或Windows API。因此一般不需要明确使用内存屏障。

 2)内存可见性问题

    主要是高速缓存与内存的一致性问题。一个处理器上的线程修改了某数据,而在另一处理器上的线程可能仍然使用着该数据在专用cache中的老值,这就是可见性出了问题。解决办法是令该数据为volatile属性,或者读该数据之前执行内存屏障。

 3) 乱序执行与编译器重排序优化的比较
      C与C++语言中,volatile关键字意图允许内存映射的I/O操作。这要求编译器对此的数据读写按照程序中的先后顺序执行,不能对volatile内存的读写重排序。因此关键字volatile并不保证是一个内存屏障。对于Visual Studio 2003,编译器保证对volatile的操作是有序的,但是不能保证处理器的乱序执行。因此,可以使用InterlockedCompareExchange或InterlockedExchange函数。对于Visual Studio 2005及以后版本,编译器对volatile变量的读操作使用acquire semantics,对写操作使用release semantics。对于Visual Studio 2003,编译器保证对volatile的操作是有序的,但是不能保证处理器的乱序执行。因此,可以使用InterlockedCompareExchange或InterlockedExchange函数。对于Visual Studio 2005及以后版本,编译器对volatile变量的读操作使用acquire semantics,对写操作使用release semantics。

4)volatile 与内存屏障

     上面我们已经提到了,指令的乱序可能由两个原因造成 编译器编译时的优化和处理器执行时的乱序优化,对于编译器编译时的优化导致的乱序我们可以用通过volatile标记,可以解决编译器层面的可见性与重排序问题,而内存屏障则解决了运行时的硬件层面的可见性与重排序问题(注:这里所指的是c/c++中的volatile关键字,在java中,jvm对volatile做了很大一部分的强化,保证编译时的乱序和执行时的处理器的乱序,其原理就是在volatile前后插入相应的内存屏障来实现的:深入理解 Java 内存模型——volatile)。

 三 创建内存屏障

   1 编译器屏障 Compiler Barrior   

   用于让编译器保证其之前的内存访问先于其之后的完成(阻止编译器重排,保证编译程序时在优化屏障之前的指令不会在优化屏障之后执行)。

/* The "volatile" is due to gcc bugs */
#define barrier() __asm__ __volatile__("": : :"memory") 

    2 CPU屏障 CPU Barrior
     CPU级别内存屏障其作用有两个: 1 防止指令之间的重排序  2 保证数据的可见性
     指令重排中Load和Store两种操作会有Load-Store、Store-Load、Load-Load、Store-Store这四种可能的乱序结果。Intel为此提供三种内存屏障指令:
     1 sfence ,实现Store Barrior 会将store buffer中缓存的修改刷入L1 cache中,使得其他cpu核可以观察到这些修改,而且之后的写操作不会被调度到之前,即sfence之前的写操作一定在sfence完成且全局可见;
     2 lfence ,实现Load Barrior 会将invalidate queue失效,强制读取入L1 cache中,而且lfence之后的读操作不会被调度到之前,即lfence之前的读操作一定在lfence完成(并未规定全局可见性);
     3 mfence ,实现Full Barrior 同时刷新store buffer和invalidate queue,保证了mfence前后的读写操作的顺序,同时要求mfence之后写操作结果全局可见之前,mfence之前写操作结果全局可见;

#define lfence() __asm__ __volatile__("lfence": : :"memory") 
#define sfence() __asm__ __volatile__("sfence": : :"memory") 
#define mfence() __asm__ __volatile__("mfence": : :"memory") 

     4 lock , 用来修饰当前指令操作的内存只能由当前CPU使用,若指令不操作内存仍然由用,因为这个修饰会让指令操作本身原子化,而且自带Full Barrior效果;还有指令比如IO操作的指令、exch等原子交换的指令,任何带有lock前缀的指令以及CPUID等指令都有内存屏障的作用。X86-64下仅支持一种指令重排:Store-Load ,即读操作可能会重排到写操作前面,同时不同线程的写操作并没有保证全局可见,例子见《Intel® 64 and IA-32 Architectures Software Developer’s Manual》手册8.6.1、8.2.3.7节。要注意的是这个问题只能用mfence解决,不能靠组合sfence和lfence解决。(用sfence+lfence组合仅可以解决重排问题,但不能解决全局可见性问题,简单理解不如视为sfence和lfence本身也能乱序重拍),X86-64一般情况根本不会需要使用lfence与sfence这两个指令,除非操作Write-Through内存或使用 non-temporal 指令(NT指令,属于SSE指令集),比如movntdq, movnti, maskmovq,这些指令也使用Write-Through内存策略,通常使用在图形学或视频处理,Linux编程里就需要使用GNC提供的专门的函数。

__asm __volatile ("lock; orl $0, (%%rsp)" ::: "memory")

      类似这种带有lock指令表示,在执行lock后面指令时,会设置处理器的LOCK#信号(这个信号会锁定总线,阻止其它CPU通过总线访问内存,直到这些指令执行结束),这条指令的执行变成原子操作,之前的读写请求都不能越过lock指令进行重排,相当于一个内存屏障Full Barrior效果。

    另外再c++11 标准库中提供了上面提到的不同内存屏障的创建方法:atomic_thread_fence(memory_order __m),具体使用方法(见文章末尾,建议把整篇文章都看了,了解不同的内存序很重要,可以更好的理解不同的内存屏障):C++11的6种内存序总结

参考文章

  C++ 多线程与内存模型资料汇

  C和C++中的volatile、内存屏障和CPU缓存一致性协议MESI

  理解Memory Barrier(内存屏障)

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

  一文解决内存屏障

  维基百科内存屏障

  聊聊原子变量、锁、内存屏障那点事

  volatile与内存屏障总结

   云凤:多核环境下的内存屏障指令

  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值