因翻译水平有限,如有不妥,敬请指正和谅解!
原文下载地址:
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中极其简陋的结构,他将非常吃惊,这样一个系统可能潜在地见到如下序列的事件:
- CPU0启动执行a=1;
- CPU0查看”a”在缓存中,并知道它未命中;
- CPU0因而发送一个”读无效”消息,为了获得包含”a”的缓存行的独占;
- CPU0记录存储”a”到它的存储缓冲中;
- CPU1接收”读无效”消息,并通过发送缓存行来响应,并从它的缓存中移除缓存行;
- CPU0启动执行b=a+1;
- CPU0从CPU1接收缓存行,这仍然是有一个0值的”a”;
- CPU0从它的缓存中装载”a”,并找到值0;
- CPU0应用它存储缓存队列中的项到新到达的缓存行,在它的缓存中设置”a”的值为1;
- CPU0加1到上面装载的”a”值0,并存储它到包含”b”的缓存行(这假设已经被CPU0所拥有);
- 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 }
6
7 void bar(void)
8 {
9 while(b==0) continue;
10 assert(a == 1);
11 }
假设CPU0执行foo()和CPU1执行bar(),进一步假设包含”a”的缓存行仅驻留在CPU1的缓存中,并且包含”b”的缓存行由CPU0所拥有,则操作序列可能如下:
- CPU0执行a=1,缓存行不在CPU0的缓存中,因此CPU0放置”a”的新值在它的存储缓冲区(store buffer),并且转发一个”读无效”消息.
- CPU1执行while(b==0)continue;但包含”b”的缓存行不在它的缓存中.它因此发送一个”读”消息.
- CPU0执行b=1,它已经拥有此缓存行(换句话说,缓存行已经在”修改”或”独占”状态),因此它存储”b”的新值在它的缓存行中.
- CPU0接收”读”消息,并发送包含最新”b”值的缓存行到CPU1,也标记为”共享”在它自己的缓存中.
- CPU1接收包含”b”的缓存行,并安装它在它的缓存.
- CPU1现在能完成执行while(b==0)continue;并因此知道”b”的值是1,它继续处理下一条语句.
- CPU1执行assert(a==1),并因此CPU1工作用”a”的旧值,此断言失败.
- CPU1接收”读无效”消息,并且发送包含”a”的缓存行到CPU0,并无效此缓存行从它所拥有的缓存,但是太迟了.
- CPU0接收包含”a”的缓存行,并立即应用缓冲的存储到断言失败的受害者CPU1.
硬件设计者不能在此直接帮助,由于CPU不知道哪个变量是相关的,更不用说它们是如何相关的.因此,硬件设计者提供内存栅栏指令去允许软件告诉CPU关于这些关系.程序片段必须被更新为包含内存栅栏:
1 void foo(void)
2 {
3 a = 1;
4 smp_mb();
5 b = 1;
6 }
7
8 void bar(void)
9 {
10 while(b == 0)continue;
11 assert(a == 1);
12 }
内存栅栏smp_mb()将引发CPU去刷新它的存储缓冲区,在应用后续存储到它们的缓存行前.在继续处理前,CPU将简单的停顿,直到存储缓冲区为空,或它可以使用存储缓冲区去持有后续存储,直到所有存储在缓冲区的先前项被应用.
因此,随后的操作序列可能如下:
CPU0执行a=1. 缓存行不在CPU0的缓存中,因此CPU0放置”a”的新值在它的存储缓冲区(store buffer)中,并发送一个”读无效”消息.
CPU1执行while(b==0)continue;但包含”b”的缓存行不在它的缓存中,它因此发送一个”读”消息.
CPU0执行smp_mp(),并且标记所有当前存储缓冲区项(即,a=1);
CPU0执行b=1,它已经拥有此缓存行(也就是说,缓存行已经在”修改”或”独占”状态),但有一个标记项在存储缓冲区(store buffer)中.因而,不是存储一个新的”b”值到缓存行,而是替代放置它d到存储缓冲区(store buffer)中(但在一个未标记的项/条目中).
CPU0接收”读”消息,并发送包含”b”原始值的缓存行到CPU1.它也标记它所拥有的缓存行副本为”共享的”.
CPU1接收包含”b”的缓存行,并装载它到缓存.
CPU1现在能完成执行while(b==0)continue;但由于它知道”b”值仍为0,它重复while语句.”b”的新值被安全隐藏在CPU0的存储缓冲区.
CPU1接收”读无效”消息,并发送包含”a”的缓存行到CPU0,并从它自己的缓存中无效此缓存行.
CPU0接收包含”a”的缓存行,并应用缓冲的存储.
由于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的值”,在处理器中也会调用大量复杂的步骤.