这篇博客是系列文章的第二篇, 主要讲一下内存屏障, 不会讲的很深, 但求明确理解和记住, 什么是内存屏障!
1. 背景知识: CPU乱序执行
这个背景知识很重要, 先讲为快, 在wiki百科上面, 关于CPU乱序执行的解释是这样的:
In computer engineering, out-of-order execution (OoOE or OOE) is a paradigm used in most high-performance microprocessors to make use of instruction cycles that would otherwise be wasted by a certain type of costly delay. In this paradigm, a processor executes instructions in an order governed by the availability of input data, rather than by their original order in a program.
In doing so, the processor can avoid being idle while data is retrieved for the next instruction in a program, processing instead the next instructions which are able to run immediately.
很多教科书上也有类似这样的解释:
对于一条指令的执行过程,通常分为:取指令、指令译码、取操作数、运算、写结果。前面三步由控制器完成,后面两步由运算器完成。按照传统的做法,当控制器工作的时候运算器在休息,在运算器工作的时候控制器在休息。流水线的做法就是当控制器完成第一条指令的操作后,直接开始开始第二条指令的操作,同时运算器开始第一条指令的操作。这样就形成了流水线系统,这是一条2级流水线。
简单的说,
乱序执行的目的, 就是为了让由不同的处理单元(比如控制器,运算器)执行的指令, 能够同时被执行, 不因为指令处于某种顺序而使得有的执行单元处于空闲状态.
举个栗子, 有如下四条指令:
1:取值A, 2:运算A, 3:回写A, 4:回写B
假设取值是由控制器执行, 运算和回写是由运算器执行.
如果按照1234的顺序执行, 那么当执行1的时候, 运算器是空闲的, 当执行234的时候, 控制器是空闲的,
一共需要4个时钟周期.
如果按照1423的顺序执行, 那么1和2就可以同时执行了, 在第1个时钟周期控制器和运算器都是空闲的, 那么,
4条指令执行下列一共需要3个时钟周期.
OK, 我想应该讲明白了乱序执行的好处. 再次向辛苦的CPU设计者致敬. 他们想出来这样的办法提高指令执行的并行性, 并且确确实实提高了程序的性能, 必然是好的事情. 但是, 在某些并行场景, 却会带来一些错误. 举一个上一篇文章提到的乱序执行例子:
int A,B;
void foo() {
A = B+1;
B = 5;
}
例子很简单, 我们想知道函数里面两行代码的指令, 是否被调换了顺序, 先看一下不用-O2优化编译的结果:
$gcc cordering.c -S && cat cordering.s
movl B(%rip), %eax
addl $1, %eax //B+1
movl %eax, A(%rip) //A=B+1
movl $5, B(%rip) //B=5
可以看到顺序并没有调换:按顺序 把B写到寄存器eax里, 把1加到寄存器eax里, 再把寄存器的值赋给A, 到这里就完成了代码A=B+1; 接着, 把5赋给B, 完成了代码B=5.
用-O2优化编译是什么情况呢:
$gcc cordering.c -S -O2 && cat cordering.s
movl B(%rip), %eax //把B存在eax
movl $5, B(%rip) //B=5,先赋值为5了!!
addl $1, %eax //eax+1, 也就是原来的B
movl %eax, A(%rip)
从上面的汇编可以看到, -O2后的代码, 执行已经被调换了:把B赋给寄存器eax, 把5赋给B, 到这里就完成了代码B=5(注意, 这个顺序变了); 接着, 把1加到寄存器eax, 最后把寄存器的值赋给A, 到这里完成了A=B+1.
通过上面的例子可以看到, 编译器的优化让指令乱序执行了, 但是不禁要问,
这有什么问题呢?
在这样一个场景下:
foo()运行在线程thread1, 有另外一个线程thread2在观察变量B, 一旦变量B变成5, thread2线程就读取A. 代码大致如下:
//全局定义:
int A,B;
//在线程thread1运行:
void foo() {
A = B+1;
B = 5;
}
//在线程thread2运行:
while(TRUE) {
if (B == 5) continue;
else fun_use(A);
}
当foo()优化编译后, 当B变成5的时候, A还没有执行A=B+1, thread2读到的就是A发生"加1"之前的值, 这明显不符合我们写这个代码的原先需求设想, 因此我们可以认为是错的. 所以我们需要引入内存屏障!
(对于代码 if (B == 5) continue; 可能会认为B没有volatile可能会被优化, 其实可以在B上面加volatile的, 或者在这行代码之前, 采取某种方法让编译器从内存里面读取B的值, 这里就做了简化 )
2、内存屏障的定义
在wiki百科上面, 内存屏障是这么定义的:
A memory barrier, also known as a membar, memory fence or fence instruction, is a type of barrier instruction which causes a central processing unit (CPU) or compiler to enforce an ordering constraint on memory operations issued before and after the barrier instruction. This typically means that certain operations are guaranteed to be performed before the barrier, and others after.
摘自《独辟蹊径品内核》:
内存屏障是指, 由于编译器的优化和缓存的使用, 导致对内存的写入操作不能及时的反应出来, 也就是说当完成对内存的写入操作之后, 读取出来的可能是旧的内容
个人认为wiki百科上面的定义更加准确.
内存屏障是一种指令, 对该指令之前和之后的内存CPU读写内存的操作, 产生一种顺序的约束. 内存屏障在一定程度上和一定范围里阻止指令的乱序, 从而阻止了CPU的乱序执行.
因为乱序可以让程序得到优化, 我们必须肯定这种机制确实是好的, 但是在有些场景却会给我们带来困扰和错误, 下面回到原来的例子, 看下内存屏障是怎么起作用的:
void foo() {
A = B+1;
B = 5;
}
我们希望这两行代码的顺序是不能调换的, 那我们就在中间插入一个"内存屏障", 内存屏障其实可以只是一行指令:
void foo() {
A = B+1;
__asm__ __volatile__("" : : : "memory"); //内存屏障
B = 5;
}
加上之后, 汇编结果如下:
movl B(%rip), %eax
addl $1, %eax //加1
movl %eax, A(%rip) //给A赋值
movl $5, B(%rip) //给B赋值
我们看到, 汇编的顺序和我们代码中我们想要的顺序是一致. 这就是内存屏障啦, 简单的说, 他就是一行阻止乱序的代码指令:)
转载请注明出处: http://blog.csdn.net/answer3y/article/details/22220299