在阅读很多底层的代码时,经常会碰到一个所谓内存屏障的概念,经常搞得一头雾水。本文将对这个概念进行一个系统的介绍。
一、为什么需要内存屏障
内存屏障的引入,本质上是由于CPU重排序指令引起的。重排序问题无时无刻不在发生,主要源自以下几种场景:
- 编译器编译时的优化;
- 处理器执行时的多发射和乱序优化;
- 读取和存储指令的优化;
- 缓存同步顺序(导致可见性问题)。
下面分别解释一下:
编译器优化
编译器在不改变单线程程序语义的前提下,也就是保证单线程程序执行结果正确的情况下,可以重新安排语句的执行顺序。编译器在优化的时候是不知道当前程序是在哪个线程中执行的,因此它只能保证单线程的正确性。
例如,有如下程序:
if (a)
b = a;
else
b = 42;
在经过编译器优化后可能变成:
b = 42;
if (a)
b = a;
这种优化在单线程下是没有问题的,但是如果有另外一个线程要读取变量b的值时,有可能会有问题。前面的程序只有当变量a的值为0时,才会将b赋值42,后面的程序无论变量a的值是多少,都有一段时间会将b赋值为42。
造成这个问题的原因是,编译器优化的时候只注重“结果”,不注重“过程”。这种优化在单线程程序中没有问题,代码一直都是自己运行,只要结果对就可以了,但是在多线程程序下,代码执行过程中的某些状态可能会对别的线程产生影响,这个是编译器优化无法考虑到的。
处理器执行时的多发射和乱序优化
现代处理器基本上都是支持多发射的,也就是在一个指令周期内可以同时执行多条指令。但是,处理器的资源就那么多,可能不能同时满足处理这些指令的要求。比如,处理器就只有一个加法器,如果同时有两条指令都需要算加法,那么有一条指令必须等待。如果这时候再下一条指令是读取指令,并且和前两条指令无关,那么这条指令将在前面某条加法指令之前完成。还有一种可能,就是前后指令之间具有相关性,比如对同一个地址先读取再写入,后面的写入操作必须等待前面的读取操作完成后才能执行。但是如果这时候第三条指令是写入一个无关的地址,那它可以在前面的写入操作之前被执行,执行顺序再次被打乱了。
所以,一般情况下指令乱序并不是CPU在执行指令之前刻意去调整顺序。CPU总是顺序的去内存里面取指令,然后将其顺序的放入指令流水线。但是指令执行时的各种条件,指令与指令之间的相互影响,可能导致顺序放入流水线的指令,最终不是按照放入的顺序执行完成,在外边看起来仿佛是“乱序”一样,这就是所谓的“顺序流入,乱序流出”。
内核资料直通车:Linux内核源码技术学习路线+视频教程代码资料
学习直通车:进程管理/内存管理/网络协议/设备驱动/文件系统/腾讯课堂
读取和存储指令的优化
CPU有可能根据情况,将相临的两条读取或写入操作合并成一条。
例如,对于如下的两条读取操作:
X = *A; Y = *(A + 4);
可能被合并成一条读取操作:
{X, Y} = LOAD {*A, *(A + 4) };
同样的,对于如下两条写入操作:
*A = X; *(A + 4) = Y;
有可能会被合并成一条:
STORE {*A, *(A + 4) } = {X, Y};
以上这几种情况,由于编译器或CPU,出于“优化”的目的,按照某种规则将指令重新排序的行为称作“真”重排序。不同的是,编译器重排序是在编译程序时进行的,一旦编译成功后执行次序就定下来了。而后面几种是在CPU运行程序时实时进行的,CPU架构不同可能起到的效果完全不同。
编译器或CPU在执行各种优化的时候,都有一些必须的前提,就是至少在单一CPU上执行不能出现问题。有一些数据访问明显是相互依赖的,就不能打乱它们的执行顺序。比如:
1)在一个给定的CPU上,有依赖的内存访问:
比如如下两条指令:
A = Load B;
C = Load *A
第二条加载指令的地址是由第一条指令加载的,第二条指令要能正确执行,必须要等到第一条指令执行完成后才行,也就是说第二条指令依赖于第一条指令。这种情况下,无论如何处理器是不会打乱这两条指令的执行次序的。不过,有可能会在这两条指令间插入别的指令,但必须保证第二条指令在第一条指令执行完后才能执行。
2)在一个给定的CPU上,交叉的加载和存储操作,它们访问的内存地址有重叠:
例如,先存储后加载同一个内存地址上的内容:
Store *X = A;
B = Load *X;
或者先加载后读取同一个内存地址上的内容:
A = Load *X;
Store *X = B;
对同一个内存地址的存取,如果两条指令执行次序被打乱了,那肯定会发生错误。但是,如果是两条加载或两条存储指令(中间没有加载),哪怕是对同一个内存地址的操作,也可能由于优化产生变化。
有了上面两条限制,很容易想到,那如果所有加载或存储指令都没有相关性呢?这时候就要看CPU的心情了,可以以任何次序被执行,可以完全不按照它们在程序中出现的次序。
技术交流免费领取资料Q群977878001
缓存同步顺序
上面的几种情况都比较好