内存屏障,也称内存栅栏,内存栅障,屏障指令等, 是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作。
简介大多数现代计算机为了提高性能而采取乱序执行,这使得内存屏障成为必须。
语义上,内存屏障之前的所有写操作都要写入内存;内存屏障之后的读操作都可以获得同步屏障之前的写操作的结果。因此,对于敏感的程序块,写操作之后、读操作之前可以插入内存屏障。
底层体系结构相关的原语大多数处理器提供了内存屏障指令:
完全内存屏障(full memory barrier)保障了早于屏障的内存读写操作的结果提交到内存之后,再执行晚于屏障的读写操作。
内存读屏障(read memory barrier)仅确保了内存读操作;
内存写屏障(write memory barrier)仅保证了内存写操作。
内存屏障是底层原语,是内存排序的一部分,在不同体系结构下变化很大而不适合推广。需要认真研读硬件的手册以确定内存屏障的办法。x86指令集中的内存屏障指令是:
lfence (asm), void _mm_lfence (void) 读操作屏障sfence (asm), void _mm_sfence (void)[1] 写操作屏障mfence (asm), void _mm_mfence (void)[2] 读写操作屏障常见的x86/x64,通常使用lock指令前缀加上一个空操作来实现,注意当然不能真的是nop指令,但是可以用来实现空操作的指令其实是很多的,比如Linux中采用的
addl $0, 0 (%esp)存储器也提供了另一套语义的内存屏障指令:
acquire semantics: 该操作结果可利用要早于代码中后续的所有操作的结果。
release semantics: 该操作结果可利用要晚于代码中之前的所有操作的结果。
fence semantics: acquire与release两种语义的共同有效。即该操作结果可利用要晚于代码中之前的所有操作的结果,且该操作结果可利用要早于代码中后续的所有操作的结果。
Intel Itanium处理器,具有内存屏障mf的指令,具有下述modifiers1:
acq (acquire)
rel (release).
Windows API的内存屏障实现下述同步函数使用适当的屏障来确保内存有序:
进出临界区(critical section)的函数
触发(signaled)同步对象的函数
等待函数(Wait function)
互锁函数(Interlocked function)1
多线程编程与内存可见性多线程程序通常使用高层程序设计语言中的同步原语,如Java与.NET Framework,或者API如pthread或Windows API。因此一般不需要明确使用内存屏障。
内存可见性问题,主要是高速缓存与内存的一致性问题。一个处理器上的线程修改了某数据,而在另一处理器上的线程可能仍然使用着该数据在专用cache中的老值,这就是可见性出了问题。解决办法是令该数据为volatile属性,或者读该数据之前执行内存屏障。2
乱序执行与编译器重排序优化的比较C与C++语言中,volatile关键字意图允许内存映射的I/O操作。这要求编译器对此的数据读写按照程序中的先后顺序执行,不能对volatile内存的读写重排序。因此关键字volatile并不保证是一个内存屏障。
对于Visual Studio 2003,编译器保证对volatile的操作是有序的,但是不能保证处理器的乱序执行。因此,可以使用InterlockedCompareExchange或InterlockedExchange函数。
对于Visual Studio 2005及以后版本,编译器对volatile变量的读操作使用acquire semantics,对写操作使用release semantics。1
编译器内存屏障编译器会对生成的可执行代码做一定优化,造成乱序执行甚至省略(不执行)。gcc编译器在遇到内嵌汇编语句:
asm volatile("" ::: "memory");将以此作为一条内存屏障,重排序内存操作。即此语句之前的各种编译优化将不会持续到此语句之后。也可用内建的__sync_synchronize
Microsoft Visual C++的编译器内存屏障为:
_ReadWriteBarrier() MemoryBarrier()Intel C++编译器的内存屏障为1:
__memory_barrier()本词条内容贡献者为:
王沛 - 副教授、副研究员 - 中国科学院工程热物理研究所