现在大多数现代计算机为了提高性能而采取乱序执行,这可能会导致程序运行不符合我们预期,内存屏障就是一类同步屏障指令,是CPU或者编译器在对内存随机访问的操作中的一个同步点,只有在此点之前的所有读写操作都执行后才可以执行此点之后的操作。
内存屏障存在的意义就是为了解决程序在运行过程中出现的内存乱序访问问题
,内存乱序访问行为出现的理由是为了提高程序运行时的性能,Memory Bariier
能够让CPU或编译器在内存访问上有序。
在进一步剖析为什么会出现内存屏障之前,如果你对Cache原理还不了解,强烈建议先阅读一下这篇文章,对Cache有了一定的了解之后,再阅读下面的内容。
早期的处理器为有序处理器(In-order processors),有序处理器处理指令通常有以下几步:
相比之下,乱序处理器(Out-of-order processors)处理指令通常有以下几步:
已经了解了cache的同学应该可以知道,如果CPU需要读取的地址中的数据已经已经缓存在了cache line中,即使是cpu需要对这个地址重复进行读写,对CPU性能影响也不大,但是一旦发生了cache miss(对这个地址进行第一次写操作),如果是有序处理器,CPU在从其他CPU获取数据或者直接与主存进行数据交互的时候需要等待不可用的操作对象,这样就会非常慢,非常影响性能。举个例子:
如果CPU0发起一次对某个地址的写操作,但是其local cache中没有数据,这个数据存放在CPU1的local cache中。为了完成这次操作,CPU0会发出一个invalidate的信号,使其他CPU的cache数据无效(因为CPU0需要重新写这个地址中的值,说明这个地址中的值将被改变,如果不把其他CPU中存放的该地址的值无效,那么就有可能会出现数据不一致的问题
)。只有当其他之前就已经存放了改地址数据的CPU中的值都无效了后,CPU0才能真正发起写操作。需要等待非常长的时间,这就导致了性能上的损耗。
但是乱序处理器山就不需要等待不可用的操作对象,直接把invalidate message放到invalidate queues中,然后继续干其他事情,提高了CPU的性能,但也带来了一个问题,就是程序执行过程中,可能会由于乱序处理器的处理方式导致内存乱序,程序运行结果不符合我们预期的问题。
Memory Barrier
能够让CPU或编译器在内存访问上有序。内存屏障包括两类:
编译器内存屏障
barrier()
用于让编译器保证其之前的内存访问先于其之后的完成。#define barrier() __asm__ __volatile__("" ::: "memory")
CPU内存屏障
mb()
和smp_mb()
wmb()
和smp_wmb()
rmb()
和smp_rmb()
有一种可以阻止CPU进入阻塞状态的方法,就是在CPU和cache之间加入一个store buffer的硬件结构,如下图:
加入了这个硬件结构后,但CPU0需要往某个地址中写入一个数据时,它不需要去关心其他的CPU的local cache中有没有这个地址的数据,它只需要把它需要写的值直接存放到store buffer中,然后发出invalidate的信号,等到成功invalidate其他CPU中该地址的数据后,再把CPU0存放在store buffer中的数据推到CPU0的local cache中。每一个CPU core都拥有自己私有的store buffer,一个CPU只能访问自己私有的那个store buffer。
先看看下面的代码,思考一下会不会出现什么问题
a = 1;
b = a + 1;
assert(b = 2);
我们假设变量a的数据存放在CPU1的local cache line中,变量b在CPU0的cache line中,如果我们单纯使用CPU来运行上面这段程序,应该是正常运行的,但是如果加入了store buffer这个硬件结构,就会出现assert失败的问题,到底是为什么呢?我们来走一走程序的流程。
硬件上出现一种新的设计,为了解决优化上面所说的store buffer的弊端, 具体结构如下图:
当CPU执行load操作的时候,不但要看cache,还要看store buffer中是否有内容,如果store buffer有该数据,那么就采用store buffer中的值。
但是这样,就能保证程序的正确运行,就能保证数据的一致性了吗?并不是!!
我们先来分析分析一个例子,看看下面的例子有可能会出现什么问题
void foo(void)
{
a = 1;
b = 1;
}
void bar(void)
{
while (b == 0) continue;
assert(a == 1);
}
同样,我们假设a和b都是初始化为0,CPU0执行foo函数,CPU1执行bar函数,a变量在CPU1的cache中,b在CPU0的cache中
assert(a==1)
,由于CPU1的local cache中a的值还是旧的值为0,assert(a==1)
失败。这个时候,就需要加入一些memory barrier的操作了。说了这么久,终于说到了内存屏障了
,我们把代码修改成下面这样:
void foo(void)
{
a = 1;
smp_mb();
b = 1;
}
void bar(void)
{
while (b == 0) continue;
assert(a == 1);
}
可以看到我们在foo函数中增加了一个smp_mb()
的操作,smp_mb有什么作用呢?
它主要是为了让数据在local cache中的操作顺序服务program order的顺序,那么它又是怎么让store buffer中存储的数据按照program order的顺序写入到cache中的呢?
我们看看数据读写流程跟上面没有加smp_mb
的时候有什么区别
因为b的最新值1还存放在store buffer中
),并设置为sharded。你以为这样就完了吗????NONONONO!!!!太天真了
不幸的是:每个CPU的store buffer不能实现地太大,其存储队列的数目也不会太多。当CPU以中等的频率执行store操作的时候(假设所有的store操作都导致了cache miss),store buffer会很快的呗填满。在这种情况下,CPU只能又进入阻塞状态,直到cacheline完成invalidation和ack的交互后,可以将store buffer的entry写入cacheline,从而让新的store让出空间之后,CPU才可以继续被执行。
这种情况也可能发生在调用了memory barrier指令之后,因为一旦store buffer中的某个entry被标记了,那么随后的store都必须等待invalidation完成,因此不管是否cache miss,这些store都必须进入store buffer。
这个时候,invalidate queues
就出现了,它可也缓解这个情况。store buffer之所以很容易被填满,主要是因为其他CPU在回应invalidate acknowledge比较慢,如果能加快这个过程,让store buffer中的内容尽快写入到cacheline,那么就不会那么容易被填满了。
而invalidate acknowledge不能尽快回复的主要原因,是因为invalidate cacheline的操作没有那么块完成,特别是在cache比较繁忙的时候,如果再收到其他CPU发来的invalidate请求,只有在完成了invalidate操作后,本CPU才会发送invalidate acknowledge。
然而,CPU其实不需要完成invalidate就可以回送acknowledgement消息,这样就不会阻止发送invalidate的那个CPU进去阻塞状态。CPU可以将这些接收到的invalidate message存放到invalidate queues中,然后直接回应acknowledge,表示自己已经收到请求,随后会慢慢处理,当时前提是必须在发送invalidate message的CPU发送任何关于某变量对应cacheline的操作到bus之前完成。结构如下图:
在ARM架构中,有3条内存屏障指令,分别是:
数据存储屏障(Data Memory Barrier,DMB)指令
数据同步屏障(Data Synchronization Barrier,DSB)指令
指令同步屏障(Instruction Synchronization Barrier,ISB)指令
未完待续…