内存屏障 memory barrier

内存屏障主要解决单处理器下的乱序问题多处理器下的内存同步问题

问题1: 为什么会乱序

编译级别的乱序以及CPU执行时的乱序

现在的CPU一般采用流水线pipeline来执行指令,一个指令的执行被分为:取指、译码、访存、执行、写回几个阶段。然后,多条指令可以同时存在于流水线中,同时被执行。

指令流水线不是串行的,并不会因为一个耗时很长的指令在执行阶段呆很长时间,而导致后续的指令都卡在执行之前的阶段上。相反,流水线是并行的,多个指令可以同时处于同一个阶段,只要CPU内部相应的处理部件未被占满即可。 比如CPU有一个加法器和一个除法器,那么一条加法指令和一条除法指令就可能同时处于执行阶段,而两条加法指令在执行阶段就只能串行工作。

但是,这样一来,乱序可能就产生了。比如一条加法指令原本出现在一条除法指令的后面,但是由于除法的执行时间很长,在它执行完之前,加法可能先执行完了。再比如两条访存指令,可能由于第二条指令命中了cache而导致它先于第一条指令完成。 一般情况下,指令乱序并不是CPU在执行指令之前可以去调整顺序。 CPU总是顺序的去内存中取指令,然后将其顺序放入指令流水线,但是指令执行时的各种条件,指令与指令之间的互相影响,可能导致顺序放入流水线的指令,最终乱序执行完成。这就是所谓的“顺序流入,乱序流出”。

CPU的乱序执行并不是任意的乱序,而是以保证程序上下文因果关系为前提的,有了这个前提,CPU执行的正确性才有保证

相比于CPU的乱序,编译器的乱序才是真正对指令顺序做了调整。但是编译器的乱序也必须保证程序上下文的因果关系不发生改变。

问题2: 乱序的后果

乱序执行,有了“保证上下文因果关系”这一前提,一般情况下是不会有问题的。因此,在绝大多数情况下,我们写程序都不会去考虑乱序所带来的影响。但是,有些程序逻辑,单纯从上下文是看不出它们的因果关系的,成为隐式的乱序。如果程序具有显式的因果关系的话,乱序一定会尊重这些关系;否则,乱序就可能打破程序原有的逻辑

多处理器情况

在多处理器下,除了每个处理器要独自面对上面讨论的问题之外,当处理器之间存在交互的时候,同样要面对乱序的问题。

举个例子,对一个变量赋值然后读出它的值这一看似“原子”的操作,因为内存是没有ALU计算单元的,所以内存没有计算的能力。而CPU一般情况下是不直接读写内存的(emmintrin.h应用例外),所以这一个过程可以看作(编译器优化后):

读取内存数据到cache –> CPU读取cache/寄存器 –> CPU的计算 –> 将结果写入cache/寄存器 –> 写回数据到内存

即: 一个处理器(记为a)对内存的写操作并不是直接就在内存上生效的,而是要先经过自身的cache。另一个处理器(记为b)如果要读取相应内存上的新值,先得等a的cache同步到内存,然后b的cache再从内存同步这个新值。而如果需要同步的值不止一个的话,就会存在顺序问题。

屏障分类

内存屏障有:读屏障、写屏障、通用屏障、优化屏障。

写屏障(write barries): 在写屏障之前的所有写操作指令都会在写屏障之后的所有写操作指令更早发生。

读屏障(read barries): 在读屏障之前的所有读操作指令都会在读屏障之后的所有读操作指令更早发生。另外,它还包含后文描述的数据依赖屏障的功能。

通用屏障(general barries): 在通用屏障之前的所有写和读操作指令都会在通用屏障之后的所有写和读操作指令更早发生。

优化屏障(optimization barrier): 编译器提供了barrier函数

文档更新时间: 2021-03-12 16:07   作者:周国强