内存栅栏:软件高手的硬件观(三)

来源:互联网 发布:重庆seo俱乐部 编辑:程序博客网 时间:2024/05/29 10:27

因翻译水平有限,如有不妥,敬请指正和谅解!
原文下载地址:
http://download.csdn.net/download/programresearch/9829674

4. 存储序列导致不必要的停顿

  不幸的是,每个存储缓冲区必须相对小,这意味着CPU执行一个适当的序列存储能填满它的储存缓冲区(例如,如果它们所有的都导致缓存未命中).在那时,CPU必须再一次等待无效去完成,为了排空它的存储缓冲,在它能继续执行前.此类情况可立即出现在内存栅栏之后,而所有后续存储指令必须等待使无效操作去完成,无论是否这些存储导致缓存未命中.
 此状态可被改进通过使标记无效确认消息尽快到达.一种完成此的方式是在每个CPU使用无效消息队列,或”无效队列”.

4.1 无效队列

无效确认消息花费如此之长(时间)的原因之一,是因为必须确保对应的缓存行实际是无效的,并且此无效会被延迟,如果缓存忙,例如,如果CPU是集中的装载和存储所有这些被放置在缓存中的数据.另外,如果大量的无效消息在短时间内到达,给定的CPU可能在进度上落后处理它们,因此可能停顿所有其他的CPU.

然而,CPU不需要实际无效缓存行,在发送确认前.它将替代使用入队列无效消息,当知道,消息将被在CPU发送关于此缓存行任何进一步的消息前处理.

4.2 无效队列和无效确认

图7显示一个有无效队列的系统.一个有无效队列的CPU可在无效消息被放置入队列中时确认,替代必须等待直到对应的缓存行实际无效.当然,CPU必须引用它的无效队列,当准备发送无效消息时—-如果对应缓存行的一项在无效队列中,CPU不能立即发送无效消息;它必须替换等待直到无效队列项被处理完成.
有无效队列的缓存.png

放置一项到无效队列本质上是CPU允许在发送关于此缓存行的任何MESI协议消息前处理此项.只要对应的数据结构没有高并发,CPU将很少因此许可被打扰.

然而,事实上无效消息被缓存到无效队列中,提供了额外的内存重排的机会,如下一节讨论.

4.3 无效队列和内存栅栏

假设”a”和”b”的值初始化为0,即”a”是只读的副本(MESI”共享”状态),并且”b”由CPU0所有(MESI”独占”或”修改”状态).接着假设,CPU0执行foo(),而CPU1执行函数bar(),按下面代码片段:

1  void foo(void)2  {3    a = 1;4    smp_mb();5    b = 1;6  }78  void bar(void)9  {10   while(b == 0) continue;11   assert(a == 1);12 }

接着操作序列可能如下:

1.CPU0执行a=1.对应的缓存行在CPU0缓存中是只读的,因此CPU0放置”a”的新值在它的存储缓冲中,并且发送一个无效消息为了刷新来自于CPU1的缓存的对应的缓存行.

2.CPU1执行while(b==0)continue,但包含”b”的缓存行不在它的缓存中.它因此发送一个”读”消息.

3.CPU0执行b=1,它已经拥有此缓存行(换句话说,缓存行已经在”修改”或”独占”状态下),因此它存储”b”的新值到它的缓存行中.

4.CPU0接收”读”消息,并发送包含最新更新的”b”的缓存行到CPU1,也在它自己的缓存中标记行为”共享”.

5.CPU1接收”a”的无效消息,放置它到它的无效队列,并发送一个”无效确认”消息到CPU0.注意:”a”的旧值仍然存在于CPU1的缓存中.

6.CPU1接收包含”b”的缓存行,并装载它到它的缓存.

7.CPU1现在可以结束执行while(b==0)continue,并由于它找到”b”的值为1,它继续下一条语句.

8.CPU1执行assert(a==1),并且由于”a”的旧值仍在CPU1的缓存中,此断言失败.

9.CPU1处理队列的”无效”消息,并从它自己的缓存中无效包含”a”的缓存行.但这太迟了.

10.CPU0接收到来至CPU0(译者注:应为CPU1)对于”a”的”无效确认”消息,并立即应用缓冲的存储到断言失败的受害者CPU1.

对此情况,CPU设计者再一次地无计可施,由于硬件不可能知道CPU放置的不同bits之间的关系.然而,内存栅栏指令能交互影响无效队列,因此当一个给定CPU执行内存栅栏,它标记所有当前在它的无效队列中的项,并强制后续任何装载去等待直到所有标记项被应用到CPU缓存.

因此,可以添加一个内存栅栏如下:

1  void foo(void)2  {3    a = 1;4    smp_mb();5    b = 1;6  }78  void bar(void)9  {10   while(b == 0)continue;11   smp_mb();12   assert(a == 1);13 }

有了这些改变,操作序列可能如下:

1.CPU0执行a=1.在CPU0的缓存中对应的缓存行是只读的,因此CPU0放置”a”的新值到它的存储缓冲中,并发送一个”无效消息”,为了刷新来至CPU1缓存的对应的缓存行.

2.CPU1执行while(b==0)continue,但包含”b”的缓存行不在它的缓存中.它因此发送一个读消息.

3.CPU0执行b=1,它已经拥有此缓存行(也就是说,缓存行已经在”修改”或”独占”状态下),因此它存储”b”的新值到它的缓存行中.

4.CPU0接收”读”消息,并发送包含最新”b”值的缓存行到CPU1,也在它自己的缓存中将行标记为”共享”.

5.CPU1接收对于”a”的”无效消息”,放置它到它的无效队列,并发送一个”无效确认”消息到CPU0.注意:”a”的旧值仍然保留在CPU1的缓存中.

6.CPU1接收包含”b”的缓存行,并装载它到它的缓存.

7.CPU1能现在完成执行while(b==0)continue;并由于它找到”b”的值为1,它继续下一条语句.

8.CPU1执行smp_mb(),标记项在它的无效队列.

9.CPU1执行assert(a==1),但由于对于包含”a”的缓存行在无效队列中有标记,CPU1必须停顿此装载直到无效队列中的项被应用.

10.CPU1处理”无效”消息,移除包含”a”的缓存行从它的缓存中.

11.CPU1现在可以自由的装载”a”的值,但由于此导致一个缓存未命中,它必须发送”读”消息去获取对应的缓存行.

12.CPU0接收来自CPU0(译者注:CPU1)对于”a”的”无效确认”消息,并因此应用缓冲存储,改变对应缓存行的MESI状态为”修改”.

13.CPU0接收来自CPU1对”a”的”读”消息,并因此改变对应缓存行的状态为”共享”,并发送缓存行到CPU1.

14.CPU1接收包含”a”的缓存行,并因此可完成装载.由于此装载返回”a”的更新值,断言通过.

有大量的MESI消息的传递,CPU得到正确的结果.

5. 读和写内存栅栏

在前面的章节,内存栅栏被同时用于标记存储缓存和无效队列的项,但在代码段中,foo()没有理由去做任何关于无效队列的事,而bar()同样没有理由去做任何关于存储队列的事.

很多CPU结构,因而提供更弱一些的内存栅栏指令,只做两者之一.大致来说,一个”读内存栅栏”仅标记无效队列,并且一个”写内存栅栏”仅标记存储缓冲.而一个完整的内存栅栏两者都做.

效果是,读内存栅栏仅排序CPU的装载并执行,因此所有在读内存栅栏之前的装载,将视为在读内存栅栏后的任何装载之前完成.类似,写内存栅栏仅排序CPU的存储,并在CPU上执行,并再一次因此所有在写内存栅栏之前的存储,将视为在写内存栅栏后的任何存储之前完成.一个完整的内存栅栏同时排序装载和存储,但仅在执行内存栅栏的CPU上.

如果更新foo和bar去使用读和写内存栅栏,显示如下:

1   void foo(void)2   {3      a = 1;4      smp_wmb();5      b = 1;6   }78   void bar(void)9   {10     while(b == 0)continue;11     smp_rmb();12     assert(a == 1);13  }

一些计算机有更多种类的内存栅栏,但通常理解这三种将提供一种好的内存栅栏引导.

原创粉丝点击