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

来源:互联网 发布:淘宝放心淘是正品吗 编辑:程序博客网 时间:2024/06/05 11:06

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

3.存储导致不必要的停顿

  虽然显示在图1中的缓存结构,对重复的从指定的CPU读和写指定的数据项提供了好的性能,但它的性能对于第一次写一个指定的缓存行却相当糟糕.为此考虑图4,它显示了由CPU0写一个由CPU1缓存持有的缓存行.由于CPU0必须等待缓存行到达,在它可写前,CPU0必须停顿一个额外的时间.
  写有不必要的延时
  但是,没有实际原因去强制CPU0去停顿如此长的时间—-毕竟,无论CPU1发送到缓存行的是什么数据,CPU0都继续无条件的覆盖它.

3.1 存储缓冲器

  一种方式去阻止此不必要的写停顿,是去添加”存储缓冲器”在各CPU和它的缓存间,如图5中所示,随着添加这些存储缓冲器,CPU0可以在它存储缓冲中,简单记录它的写数据,并继续执行.当缓存行最终从CPU1移动到CPU0时,数据将从存储缓冲区被移动到缓存行.
  有存储缓冲的缓存
  然而,有一些难题必须被解决,这些在后面两节中讲到.

3.2 存储转发

来看第一个难题,违反自洽性,考虑如下代码,有变量”a”和”b”都初始化为0,并且包含变量”a”的缓存行初始由CPU1所拥有,包含变量”b”的初始由CPU0拥有:

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

没人希望断言失败.然而,如果有人非常愚蠢的使用图5中极其简陋的结构,他将非常吃惊,这样一个系统可能潜在地见到如下序列的事件:

  1. CPU0启动执行a=1;
  2. CPU0查看”a”在缓存中,并知道它未命中;
  3. CPU0因而发送一个”读无效”消息,为了获得包含”a”的缓存行的独占;
  4. CPU0记录存储”a”到它的存储缓冲中;
  5. CPU1接收”读无效”消息,并通过发送缓存行来响应,并从它的缓存中移除缓存行;
  6. CPU0启动执行b=a+1;
  7. CPU0从CPU1接收缓存行,这仍然是有一个0值的”a”;
  8. CPU0从它的缓存中装载”a”,并找到值0;
  9. CPU0应用它存储缓存队列中的项到新到达的缓存行,在它的缓存中设置”a”的值为1;
  10. CPU0加1到上面装载的”a”值0,并存储它到包含”b”的缓存行(这假设已经被CPU0所拥有);
  11. CPU0执行assert(b==2),这将失败;

 问题在于有两个”a”的副本,一个在缓存中,另一个在存储缓冲区中.
 此例违反了一个非常重要的保证,称为各CPU将总看到它自己的操作,就像它们是按程序顺序发生的.
 这种保证对软件类型来说是极为直观的,因此硬件同事怜惜,并实现了”存储转发”,当执行装载时,每个CPU引用(或”检测”)它自己的存储缓冲区(store buffer),如同它的缓存一样,如图6中所示.也就是说,一个给定”CPU”的存储是直接转发给它自己后续装载,而不必传递到缓存.
有存储转发的缓存
由于在相应位置有存储转发,上面序列的第8项,将在存储缓冲区找到正确的”a”值,因此”b”的最终值将是2,如所期待的.

3.3 存储缓冲区和内存栅栏
来看第二个难题,违反全局内存排序,考虑如下代码序列,变量”a”和”b”初始化为0:

1  void foo(void)2  {3    a = 1;4    b = 1;5  }67  void bar(void)8  {9    while(b==0) continue;10   assert(a == 1);11 }

假设CPU0执行foo()和CPU1执行bar(),进一步假设包含”a”的缓存行仅驻留在CPU1的缓存中,并且包含”b”的缓存行由CPU0所拥有,则操作序列可能如下:

  1. CPU0执行a=1,缓存行不在CPU0的缓存中,因此CPU0放置”a”的新值在它的存储缓冲区(store buffer),并且转发一个”读无效”消息.
  2. CPU1执行while(b==0)continue;但包含”b”的缓存行不在它的缓存中.它因此发送一个”读”消息.
  3. CPU0执行b=1,它已经拥有此缓存行(换句话说,缓存行已经在”修改”或”独占”状态),因此它存储”b”的新值在它的缓存行中.
  4. CPU0接收”读”消息,并发送包含最新”b”值的缓存行到CPU1,也标记为”共享”在它自己的缓存中.
  5. CPU1接收包含”b”的缓存行,并安装它在它的缓存.
  6. CPU1现在能完成执行while(b==0)continue;并因此知道”b”的值是1,它继续处理下一条语句.
  7. CPU1执行assert(a==1),并因此CPU1工作用”a”的旧值,此断言失败.
  8. CPU1接收”读无效”消息,并且发送包含”a”的缓存行到CPU0,并无效此缓存行从它所拥有的缓存,但是太迟了.
  9. CPU0接收包含”a”的缓存行,并立即应用缓冲的存储到断言失败的受害者CPU1.

硬件设计者不能在此直接帮助,由于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   assert(a == 1);12 }

内存栅栏smp_mb()将引发CPU去刷新它的存储缓冲区,在应用后续存储到它们的缓存行前.在继续处理前,CPU将简单的停顿,直到存储缓冲区为空,或它可以使用存储缓冲区去持有后续存储,直到所有存储在缓冲区的先前项被应用.
因此,随后的操作序列可能如下:

  1. CPU0执行a=1. 缓存行不在CPU0的缓存中,因此CPU0放置”a”的新值在它的存储缓冲区(store buffer)中,并发送一个”读无效”消息.

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

  3. CPU0执行smp_mp(),并且标记所有当前存储缓冲区项(即,a=1);

  4. CPU0执行b=1,它已经拥有此缓存行(也就是说,缓存行已经在”修改”或”独占”状态),但有一个标记项在存储缓冲区(store buffer)中.因而,不是存储一个新的”b”值到缓存行,而是替代放置它d到存储缓冲区(store buffer)中(但在一个未标记的项/条目中).

  5. CPU0接收”读”消息,并发送包含”b”原始值的缓存行到CPU1.它也标记它所拥有的缓存行副本为”共享的”.

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

  7. CPU1现在能完成执行while(b==0)continue;但由于它知道”b”值仍为0,它重复while语句.”b”的新值被安全隐藏在CPU0的存储缓冲区.

  8. CPU1接收”读无效”消息,并发送包含”a”的缓存行到CPU0,并从它自己的缓存中无效此缓存行.

  9. CPU0接收包含”a”的缓存行,并应用缓冲的存储.

  10. 由于a的存储,是在存储缓冲区中仅有的被smp_mb()标记的项,CPU0也能存储”b”的新值–除了实际上包含”b”的缓存行现在是”共享”状态.

11.CPU0因此发送一个”无效”消息到CPU1

12.CPU1接收”无效”消息,从缓存中无效包含”b”的缓存行,并发送一个”确认”消息到CPU0.

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

14.CPU0接收”确认”消息,并设置包含”b”的缓存行为”独占”状态.CPU0现在存储”b”的新值到缓存行.

15.CPU0接收”读”消息,并发送包含原始”b”值的缓存行到CPU1,它也标记它的缓存行副本为”共享”.

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

17.CPU1现在能结束执行while(b==0)continue;并且由于它知道”b”的值为1,它继续下一条语句.

18.CPU1执行assert(a==1),但包含”a”的缓存行不在它的缓存中.一旦它从CPU0获得此缓存,它将用”a”的最新值工作,并且断言因此通过.

如你所见,此过程调用了不少的记录.即使一些直观上简单的,如”装载a的值”,在处理器中也会调用大量复杂的步骤.