CPU

时间:2016-01-31 15:35:50

标签: synchronization locking cpu-architecture lock-free memory-barriers

我一直在阅读Memory Barriers: A Hardware View For Software Hackers,这是Paul E. McKenney的一篇非常受欢迎的文章。

本文强调的一点是,非常弱的处理器(如Alpha)可以重新排序依赖负载,这似乎是分区缓存的副作用

论文摘录:

1 struct el *insert(long key, long data)
2 {
3     struct el *p;
4     p = kmalloc(sizeof(*p), GPF_ATOMIC);
5     spin_lock(&mutex);
6     p->next = head.next;
7     p->key = key;
8     p->data = data; 
9     smp_wmb();
10    head.next = p;
11    spin_unlock(&mutex);
12 }
13
14 struct el *search(long key)
15 {
16     struct el *p;
17     p = head.next;
18     while (p != &head) {
19         /* BUG ON ALPHA!!! */
20         if (p->key == key) {
21             return (p);
22         }
23         p = p->next;
24     };
25     return (NULL);
26 }
  1. CPU0和CPU1共有2个处理器。
  2. 每个CPU有2个缓存库CB0(奇数地址),CB1(偶数地址)。
  3. 头部位于CB0,CB1位于CB1。
  4. insert()有一个写屏障,确保第6-8行的无效首先在总线中,然后在第10行失效。
  5. 但是,执行搜索的其他处理器可以轻载CB0并且CB1负载很重。
  6. 这意味着处理器引导头的最新值但是p的旧值(因为p的失效请求尚未由CB1处理。)
  7. 问题: 看起来所有架构都期望Alpha荣誉依赖负载。 例如:IA64可以重新排序以下内容,但是依赖负载重新排序。

    1. 加载后重新加载的负载
    2. 存储后重新加载
    3. 商店后重新订购的商店
    4. 加载后重新排序的商店
    5. 用载荷重新排序的原子指令。
    6. 原子指令与商店重新订购。
    7. 这让我想知道需要哪些硬件支持来防止依赖负载重新排序。

      一个可能的答案是所有其他架构(IA64)没有分区缓存,因此不会遇到此问题,也不需要明确的硬件支持。

      任何见解?

1 个答案:

答案 0 :(得分:10)

简答:

在无序处理器中,加载 - 存储队列用于跟踪和强制执行内存排序约束。 Alpha 21264等处理器具有防止相关负载重新排序的必要硬件,但强制执行此依赖性可能会增加处理器间通信的开销。

答案很长:

依赖性跟踪的背景

最好用一个例子来解释。想象一下,您有以下指令序列(为简单起见使用伪代码指令):

ST R1, A       // store value in register R1 to memory at address A
LD B, R2       // load value from memory at address B to register R2
ADD R2, 1, R2  // add immediate value 1 to R2 and save result in R2

在此示例中,LDADD指令之间存在依赖关系。 ADD读取R2的值,因此在LD使该值可用之前无法执行。这种依赖关系是通过寄存器进行的,处理器的问题逻辑可以跟踪它。

但是,如果地址STLD相同,则AB之间也可能存在依赖关系。但与LDADD之间的依赖关系不同,STLD之间的可能依赖关系在发出指令时是未知的(开始执行)

处理器不使用在发布时检测内存依赖性,而是使用称为加载存储队列的结构来跟踪它们。此结构的作用是跟踪已发布但尚未停用的指令的挂起加载和存储的地址。如果存在内存订购违规,则可以检测到这种情况,并且可以从发生违规的位置重新开始执行。

回到伪代码示例,您可以想象在LD之前执行ST的情况(可能由于某些原因,R1中所需的值尚未准备好)。但是当ST执行时,它会看到地址AB相同。所以LD应该真正读取ST生成的值,而不是缓存中已经存在的陈旧值。因此,需要重新执行LD以及LD之后的任何说明。可以通过各种优化来减少一些开销,但基本思想仍然存在。

正如我前面提到的,检测这种依赖性的逻辑存在于允许推测执行内存指令(包括Alpha处理器)的所有无序处理器中。

内存排序规则

但是,内存排序规则并不仅仅限制处理器从其自己的内存操作中看到结果的顺序。相反,内存排序规则限制了操作的相对顺序,在一个处理器上执行的内存操作变得对其他处理器可见。

Alpha示例

在依赖负载重新排序的情况下,处理器必须跟踪此信息以供自己使用,但Alpha ISA不要求它确保其他处理器看到此顺序。如何发生这种情况的一个例子如下(我引用了this link

Initially: p = & x, x = 1, y = 0

    Thread 1         Thread 2
--------------------------------
  y = 1         |    
  memoryBarrier |    i = *p
  p = & y       |
--------------------------------
Can result in: i = 0
  

此异常行为目前仅适用于基于21264的行为   系统。显然你必须使用我们的多处理器之一   服务器。最后,你实际看到它的可能性非常低,   但它是可能的。

     

以下是显示此行为的必要条件。假设T1   P2上的P1和T2运行。 P2必须缓存位置y,值为0。   P1确实y = 1,这导致"无效y"被送到P2。这个   invalidate进入传入的"探测队列" P2;随你便   看,问题出现是因为理论上这个无效   坐在探测队列中而不在P2上执行MB。无效是   此时立即承认(即,你不等待它   实际上在发送之前使P2的缓存中的副本无效   确认)。因此,P1可以通过它的MB。它继续下去   写p。现在P2继续读取p。阅读回复p   允许在其传入路径上绕过P2上的探测队列(这个   允许回复/数据快速返回21264而无需   等待以前的传入探测器被服务)。现在,P2可以   derefence P读取位于其缓存中的y的旧值   (P2的探测队列中的inval y仍然在那里)。

     

P2上的MB如何修复此问题? 21264刷新其传入的探测器   每个MB都有队列(即服务于那里的任何未决消息)。   因此,在读取P之后,你会做一个MB,它将inval拉入y   当然。并且您无法再看到y的旧缓存值。

     

尽管上述情况在理论上是可行的,但机会很大   观察由此引起的问题非常微小。原因是   即使您正确设置了缓存,P2也可能有足够的空间   在其探测队列中服务消息(即inval)的机会   在收到&#34之前的数据回复之前;阅读p"。尽管如此,如果你   进入你在P2探测器中放置了很多东西的情况   在inval之前排队到y,那么有可能是对p的回复   回来并绕过这个问题。你很难   虽然设置了场景并实际观察了异常现象。

     

上述内容解决了当前Alpha可能违反您所拥有的内容的方式   所示。由于其他优化,未来的Alpha可以违反它。一   有趣的优化是价值预测。

摘要

强制执行相关负载排序所需的基本硬件已经存在于所有无序处理器中。但是确保所有处理器都能看到这种内存排序会增加处理缓存行无效的额外限制。并且它可能在其他场景中添加额外的约束。然而,在实践中,弱的Alpha内存模型对于硬件设计人员的潜在优势似乎不值得花费软件复杂性并增加额外的开销,需要更多的内存障碍。