内存访问乱序主要是为了提升程序运行时的性能,内存乱序访问主要在两个地方:
1. 编译时,编译器优化导致内存乱序访问(指令重排)
可以使用barrier()阻止编译器优化
2. 运行时,多cpu间交互引起内存乱序访问
使用mb/wmb/rmb刷新数据cache,保证mb/wmb/rmb之前的读写操作在后面的读写操作前完成
总之,barrier()是编译器屏障,*mb是cpu屏障
编译时内存乱序访问
// test.cpp
int x, y, r;
void f()
{
x = r;
y = 1;
}
首先编译此源文件,g++ -S test.cpp
得到汇编代码如下:
movl r(%rip), %eax
movl %eax, x(%rip)
movl $1, y(%rip)
这里我们可以看到x=r和y=1并没有乱序,现使用优化选项O2或者O3编译上面的代码,g++ -O2 -S test.cpp,生成的汇编代码如下:
movl r(%rip), %eax
movl $1, y(%rip)
movl %eax, x(%rip)
我们可以清楚的看到经过编译器优化之后 movl $1, y(%rip) 先于 movl %eax, x(%rip) 执行。避免编译时内存乱序访问的办法就是使用编译器 barrier(又叫优化 barrier)。Linux 内核提供函数 barrier() 用于让编译器保证其之前的内存访问先于其之后的完成。内核实现 barrier() 如下(X86-64 架构):
#define barrier() __asm__ __volatile__("" ::: "memory")
现在把此编译器 barrier 加入代码中:
int x, y, r;
void f()
{
x = r;
__asm__ __volatile__("" ::: "memory");
y = 1;
}
这样就避免了编译器优化带来的内存乱序访问的问题了(如果有兴趣可以再看看编译之后的汇编代码)。
运行时内存乱序访问
在单个 CPU 上,硬件能够保证程序执行时所有的内存访问操作看起来像是按程序代码编写的顺序执行的,这时候CPU Memory barrier 没有必要使用,而只要考虑防止编译器优化而导致的乱序。
在单 CPU 上,不考虑编译器优化导致乱序的前提下,多线程执行不存在内存乱序访问的问题。我们从内核源码也可以得到类似的结论(代码不完全的摘录):
#ifdef CONFIG_SMP
#define smp_mb() mb()
#else
#define smp_mb() barrier()
#endif
这里可以看到,如果是 SMP 则使用 mb,mb 被定义为 CPU Memory barrier(后面会讲到),而非 SMP 时,直接使用编译器 barrier。
对于多个cpu的情况,我们使用CPU Memory barrier来解决内存乱乱序访问的问题(X86-64 架构下):
void run1()
{
x = 1;
__asm__ __volatile__("mfence" ::: "memory");
r1 = y;
}
void run2()
{
y = 1;
__asm__ __volatile__("mfence" ::: "memory");
r2 = x;
}