为什么要用Memory barrier

来源:互联网 发布:mac迅雷速度慢 编辑:程序博客网 时间:2024/05/29 09:32
  • 1 从一段代码说起
  • 2 从硬件角度理解Memory Barriers
    • 2.1 Cache的结构
    • 2.2 缓存一致性协议(Cache-Coherence Protocols)
      • 2.2.1 MESI状态
      • 2.2.2 MESI协议消息
      • 2.2.3 MESI协议的实例
    • 2.3 存储导致的不必要的拖延
      • 2.3.1 引入Store Buffers
      • 2.3.2 存储转发(Store forwarding)
      • 2.3.3 Store Buffers 和 Memory Barriers
    • 2.4 序列存储导致的不必要拖延
      • 2.4.1 Invalidate Queues
      • 2.4.2 Invalidate Queues 和 Memory Barriers
    • 2.5 Read和write Memory Barriers
  • 3 回到最开始的代码

1 从一段代码说起

代码如下:

//~ reordering.cpp//compile: g++ -pthread reordering.cpp#include <iostream>#include <semaphore.h>#include <sys/time.h>using namespace std;sem_t mtx;sem_t begin1;sem_t begin2;sem_t end;struct timeval start;struct timeval endt;unsigned long timer;volatile int x = 0, y = 0, r1 = 0, r2 = 0;void *thread1(void *) {  while (true) {    sem_wait(&begin1);    sem_wait(&mtx);    x = 1;    //asm volatile("mfence" ::: "memory"); //~ barrier to the processors    r1 = y;    sem_post(&mtx);    sem_post(&end);  }  return NULL;}void *thread2(void *) {  while (true) {    sem_wait(&begin2);    sem_wait(&mtx);    y = 1;    //asm volatile("mfence" ::: "memory"); //~ barrier to the processors    r2 = x;    sem_post(&mtx);    sem_post(&end);  }  return NULL;}intmain(int argc, char *argv[]) {  sem_init(&mtx, 0, 1);  sem_init(&begin1, 0, 0);  sem_init(&begin2, 0, 0);  sem_init(&end, 0, 0);  pthread_t t1, t2;  pthread_create(&t1, NULL, thread1, NULL);  pthread_create(&t2, NULL, thread2, NULL);  int detected = 0;  gettimeofday(&start,NULL);  for (int iterations = 1; ; iterations++) {    x = y = 0;    //~ sync beginning    sem_post(&begin1);    sem_post(&begin2);    //~ sync end    sem_wait(&end);    sem_wait(&end);    if (r1 == 0 && r2 == 0) {      ++detected;      cerr<<detected<<" reordering detected out of "<<iterations<<" iterations"<<endl;;      cerr<<"one time per "<<iterations/detected<<" iteration"<<endl;    }  }  return 0;}

重点在两个thread上,我用semaphore来控制两个线程能同时运行,而且在一次循环中相互等待结束,一次来保证同步。两个thread做的事情简化如下:

thread1:x = 1;r1 = y;thread2:y = 1;r2 = y;

如果按照常理,代码正常之行下去不会出现: r1 == 0 && r2 == 0的情况。但是运行此代码发现几乎每次循环都会出错。为什么? 因为x86的处理器会在执行代码时乱序执行,这种现象在 先写后读的情况下尤为严重。
也就是说,根本都还没有对x y进行赋值就已经开始读取x y了。
但是为什么会发生这样的情况呢?一般来说我们知道这些就够了,但是我希望在下面我能从硬件(cache 的架构)角度解释这个现象。

2 从硬件角度理解Memory Barriers

2.1 Cache的结构

现代CPU要比内存的执行速度快出来很多,以2006年一个CPU为例,CPU 1 纳秒能执行10条指令,但是从main memory取一个数据要花数十纳秒。因此为来加快处理器的执行速度,在处理器和CPU之间就有了几兆字节的缓存。
这些cache通常能在几个指令周期内就被访问到。(缓存有分L1,L2,等等,L1一般单个周期就能访问到,而L2的可能就需要将近10个时钟周期,高性能的CPU一般有3层甚至4层的缓存.)

在CPU缓存和内存中流动的固定长度的数据块就叫做cache lines,通常都是2的指数倍的大小,从16到256字节不等。当一个数据块第一次被一个CPU访问的时候,它肯定不位于CPU的缓存中,那就意味着发生了一次cache miss,或者更严谨点说是一次startup或者warmup的cache miss(又称为compulsory miss),这意味着CPU就要等几百个时钟周期去取回这个数据。但这个数据就会被存下来以供以后快速的获取。

但是缓存总会满,接下来需要再写入缓存时就会发生capacity miss.(可以从字面上来理解是由于容量问题而导致的miss)

此外还有write miss,是在CPU执行写操作但是发现数据是可读时发生。而且一个CPU修改(invalidate)了其他CPU的缓存,但随后其他CPU需要访问该数据时,会发生communication miss. (这个miss就是因为CPU之间对于cache的沟通不够而导致的)

因此,维护CPU之间缓存的一致性就非常重要,通俗点说,每个CPU都有自己的快速访问区(cache),但是每个CPU一定要商量好这些缓存的数据都是正确的,不能因为快就丧失了正确性,但是内存只有一块,一个CPU通过cache去改,另外的也通过自己的cache去改,就会产生问题。
所幸,这些问题都可以通过缓存一致性协议(cache-coherence protocols)解决。

2.2 缓存一致性协议(Cache-Coherence Protocols)

我们暂时只关注这个协议中的四状态MESI协议
建议这部分可以大致浏览,暂时不要细细品读,结合后边的例子再来阅读。

2.2.1 MESI状态

MESI表示modified,exclusive,shared,invalid.
modified表示该cache-line包含修改过的数据,内存中的数据不会出现在其他CPU-cache中,此时该CPU的cache中包含的数据是最新的.

exclusive与modified类似,但是数据没有修改,表示内存中的数据是最新的。如果此时要从cache中剔除数据项,不需要将数据写回内存。

shared数据项可能在其他CPU中有重复,CPU必须在查询了其他CPU之后才可以向该cache-line写数据.

invalid表示该cache-line为空,可以直接使用。

因为所有的CPU必须维护缓冲一致性视图,所以此协议提供了一些消息(messages)进行协调。

2.2.2 MESI协议消息

read: 包含要读取的CACHE-LINE的物理地址

invalidate: 包含要invalidate的cache-line的物理地址,所有其他cache必须移除相应的数据项

read invalidate: 包含要读取的cache-line的物理地址,同时使其他cache移除该数据。需要read response和invalidate ack消息

writeback:包含要写回的数据和地址,该状态将处于modified状态的lines写回内存,为其他数据腾出空间

read response: 包含READ请求的数据,要么由内存满足要么由cache满足

invalidate ack: 回复消息

2.2.3 MESI协议的实例

如图
Initially, the CPU cache lines in which the data would reside are in the “invalid” state, and the data is valid in memory. When CPU 0 loads the data at address 0, it enters the “shared” state in CPU 0’s cache, and is still valid in memory.
CPU 3 also loads the data at address 0, so that it is in the “shared” state in both CPUs’ caches, and is still valid in mem- ory. Next CPU 0 loads some other cache line (at ad- dress 8), which forces the data at address 0 out of its cache via an invalidation, replacing it with the data at address 8.
CPU 2 now does a load from address 0, but this CPU realizes that it will soon need to store to it, and so it uses a “read invalidate” message in order to gain an exclusive copy, invalidating it from CPU 3’s cache (though the copy in memory remains up to date).
Next CPU 2 does its anticipated store, changing the state to “modified”. The copy of the data in memory is now out of date. CPU 1 does an atomic increment, using a “read invalidate” to snoop the data from CPU 2’s cache and invalidate it, so that the copy in CPU 1’s cache is in the “modified” state (and the copy in memory remains out of date).
Finally, CPU 1 reads the cache line at address 8, which uses a “writeback” message to push address 0’s data back out to memory.

2.3 存储导致的不必要的拖延

2.3.1 引入Store Buffers

其实在进行invalidate操作的时候,cpu0要等待cpu1传回应答之后才能执行写操作(因为我得保证其他的CPU得抹去这个数据,我才好写新数据),这个期间cpu0都是闲置的,但是事实上这时没有必要的,因为不管cpu1传回什么数据,cpu0都是要进行重写的(因为CPU0只是为了保证CPU1赶快擦去老数据),因此就要引入store buffers
cpu0可以不用在等到应答之后才取写数据,而是直接写好数据,但是先不存在cache line当中,先放入store buffer中

2.3.2 存储转发(Store forwarding)

但是这样设计仍然会有缺陷,考虑如下代码:

1 a = 1;2 b = a + 1;3 assert(b == 2);  

假设初始时a和b的值都是0,a处于CPU1-cache中,b处于CPU0-cache中。如果按照下面流程执行这段代码:

1 CPU0执行a=1;2 因为a在CPU1-cache中,所以CPU0发送一个read invalidate消息来占有数据3 CPU0将a存入store buffer4 CPU1接收到read invalidate消息,于是它传递cache-line,并从自己的cache中移出该cache-line5 CPU0开始执行b=a+1;6 CPU0接收到了CPU1传递来的cache-line,即“a=0”7 CPU0从cache中读取a的值,即“0”8 CPU0更新cache-line,将store buffer中的数据写入,即“a=1”9 CPU0使用读取到的a的值“0”,执行加1操作,并将结果“1”写入b(b在CPU0-cache中,所以直接进行)10 CPU0执行assert(b == 2); 失败  

出现问题的原因是我们有两份”a”的拷贝,一份在cache-line中,一份在store buffer中。硬件设计师的解决办法是store forwarding,当执行load操作时,会同时从cache和store buffer里读取。也就是说,当进行一次load操作,如果store-buffer里有该数据,则CPU会从store-buffer里直接取出数 据,而不经过cache。因为“store forwarding”是硬件实现,我们并不需要太关心。

2.3.3 Store Buffers 和 Memory Barriers

但是这样仍然还会有问题,考虑如下代码:

void foo(void){a = 1;b = 1;}void bar(void){while (b == 0) continue;assert(a == 1);}

假设变量a在CPU1-cache中,b在CPU0-cache中。CPU0执行foo(),CPU1执行bar(),程序执行的顺序如下:

1 CPU0执行 a = 1; 因为a不在CPU0-cache中,所以CPU0将a的值放到store-buffer里,然后发送read invalidate消息2 CPU1执行while(b == 0) continue; 但是因为b不再CPU1-cache中,所以它会发送一个read消息3 CPU0执行 b = 1;因为b在CPU0-cache中,所以直接存储b的值到store-buffer中4 CPU0收到 read 消息,于是它将更新过的b的cache-line传递给CPU1,并标记为shared5 CPU1接收到包含b的cache-line,并安装到自己的cache中6 CPU1现在可以继续执行while(b == 0) continue;了,因为b=1所以循环结束7 CPU1执行assert(a == 1);因为a本来就在CPU1-cache中,而且值为0,所以断言为假8 CPU1收到read invalidate消息,将并将包含a的cache-line传递给CPU0,然后标记cache-line为invalid。但是已经太晚了  

就是说,可能出现这类情况,b已经赋值了,但是a还没有,所以出现了b = 1, a = 0的情况。对于这类问题,硬件设计者也爱莫能助,因为CPU无法知道变量之间的关联关系。所以硬件设计者提供了memory barrier指令,让软件来告诉CPU这类关系。解决方法是修改代码如下:

void foo(void){a = 1;smp_mb();b = 1;}  

smp_mb()指令可以迫使CPU在进行后续store操作前刷新store-buffer。以上面的程序为例,增加memory barrier之后,就可以保证在执行b=1的时候CPU0-store-buffer中的a已经刷新到cache中了,此时CPU1-cache中的a 必然已经标记为invalid。对于CPU1中执行的代码,则可以保证当b==0为假时,a已经不在CPU1-cache中,从而必须从CPU0- cache传递,得到新值“1”。具体参考[1]

2.4 序列存储导致的不必要拖延

但是不幸的是,store buffer都相对较小,一个CPU执行不了多少序列就会填满这个缓冲区,一旦填满,CPU就必须等待invalidation的回应才能释放store buffer,同样的情形也发生在memory barrier执行之后,后续的store操作必须等待invalidation操作完成,不管这些store操作能否导致cache miss。
解决这个问题的办法是增加一个invalidate queues

2.4.1 Invalidate Queues

之所以invalidate acknowledge message要花费很长的时间是因为他们必须确认相对应的cache line确实是被invalidated了,而如果cache比较忙的话,invalidation就会被延迟。
然而CPU不需要在发出acknowledgement之前真正的invalidate相应的cache line,它可以用一个队列来先存储这些信息,但它也明白之后一定会去处理的。

2.4.2 Invalidate Queues 和 Memory Barriers

Invalidate Queues就没有问题了吗?不!看下面的例子。

void foo(void){a = 1;smp_mb();b = 1;}void bar(void){while (b == 0) continue;assert(a == 1);}  

a处于shared状态,b在CPU0-cache内。CPU0执行foo(),CPU1执行函数bar()。执行操作如下:

1 CPU0执行a=1。因为cache-line是shared状态,所以新值放到store-buffer里,并传递invalidate消息来通知CPU12 CPU1执行 while(b==0) continue;但是b不再CPU1-cache中,所以发送read消息3 CPU1接受到CPU0的invalidate消息,将其排队,然后返回ACK消息5 CPU0执行b=1;因为已经包含了该cache-line,所以将b的新值写入cache-line6 CPU0接收到了read消息,于是传递包含b新值的cache-line给CPU1,并标记为shared状态7 CPU1接收到包含b的cache-line8 CPU1继续执行while(b==0) continue;因为为假所以进行下一个语句9 CPU1执行assert(a==1),因为a的旧值依然在CPU1-cache中,断言失败10 尽管断言失败了,但是CPU1还是处理了队列中的invalidate消息,并真的invalidate了包含a的cache-line,但是为时已晚  

可以看出出现问题的原因是,当CPU排队某个invalidate消息后,在它还没有处理这个消息之前,就再次读取该消息对应的数据了,该数据此时本应该已经失效的。

解决方法是在bar()中也增加一个memory barrier:

void bar(void){while (b == 0) continue;smp_mb();assert(a == 1);}  

此处smp_mb()的作用是处理“Invalidate Queues”中的消息,于是在执行assert(a==1)时,CPU1中的包含a的cache-line已经无效了,新的值要重新从CPU0-cache中读取。所以其实我们可以发现cache的整个发展模式,就是尽量减少因为不必要的等待而加快回应,与此同时在读写两端都加入buffer(读端为invalidate queues,写端为store buffer),但是为了保证缓冲区的内容一定会被执行,加入了MB

2.5 Read和write Memory Barriers

在之前的例子当中我们可以发现其实foo()不参与处理invalidate queue,而bar()不参与处理store queue,因此很多CPU架构就提供了相对弱一点的memory barriers,也就是说一个 read memory barrier只标记invalidate queue,而write memory barriers只标记store buffer,而全功能的memory ba1rriers两者都做。
换句话讲,如果只有Read mb, 那么就能保证在rmb之前执行的load操作(read)一定比在rmb之后的load操作先完成,同理Write mb也可类比。

可以使用rmb和wmb重写上面的例子:

void foo(void){a = 1;smp_wmb();b = 1;}void bar(void){while (b == 0) continue;smp_rmb();assert(a == 1);}

3 回到最开始的代码

我们已经深入了解了cache的结构,那我们现在就来分析一下本文最开始的地方为什么会出现这样的错误,当然是从cache的角度来分析:

cpu0            cpu1x = 1          y = 1r1 = y         r2 = x

1) 初始状态为x=y=0,且在cache中都是属于shared状态。
2)cpu0要对x赋值,发出validate信息(为什么不发出read invalidate,是因为在cache 中有x,所以只需要invalidate就可以了),同时将x = 1存入store buffer,然后执行下一条指令,赋r1值为0
3)cpu1要对y赋值,发出validate信息,同时将y = 1存入store buffer,然后执行下一条指令,赋r2值为0
4)此时cpu1接到了read invalidate信息,清除x的cache line,但是已经晚了。
5)此时cpu0也接到了read invalidate信息,清除y的cache line,但是也已经晚了。

原来是这样!所以我们可以发现,如此分析的话,不出错才是不合理的事情,那我们使用mb来重新运行此段代码(去掉程序中的注释即可),发现完全不会再出错,因为程序在执行到mb的时候就已经flush了store buffer里边的内容,这就是一个强制的过程,不会允许store buffer里还有标记的内容就去读取x 和 y的值。一旦刷新了store buffer,程序当然就不会发生了错误。

Paul在他的文章里边也全面描述了两变量MB的所有场景,具体可参考附录5,我们开头举的例子就是最常见的 Scenario 10

参考资料:

[1]http://www.rdrop.com/users/paulmck/scalability/paper/whymb.2010.06.07c.pdf

[2]汤普金斯之路 --- http://sstompkins.wordpress.com/2011/04/12/why-memory-barrier?/

[3]原子,锁,还有内存屏障 --- http://www.dutor.net/index.php/2013/10/atomic-lock-memory-barrier/

[4]内存屏障 --- http://ifeve.com/memory-barriers-or-fences/

[5]User-space RCU: Memory-barrier menagerie --- https://lwn.net/Articles/573436/

【装载】http://scriptogr.am/yifan/post/whymb,很不错

0 0
原创粉丝点击