我一直在阅读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 }
问题: 看起来所有架构都期望Alpha荣誉依赖负载。 例如:IA64可以重新排序以下内容,但是依赖负载重新排序。
这让我想知道需要哪些硬件支持来防止依赖负载重新排序。
一个可能的答案是所有其他架构(IA64)没有分区缓存,因此不会遇到此问题,也不需要明确的硬件支持。
任何见解?
答案 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
在此示例中,LD
和ADD
指令之间存在依赖关系。 ADD
读取R2
的值,因此在LD
使该值可用之前无法执行。这种依赖关系是通过寄存器进行的,处理器的问题逻辑可以跟踪它。
但是,如果地址ST
和LD
相同,则A
和B
之间也可能存在依赖关系。但与LD
和ADD
之间的依赖关系不同,ST
和LD
之间的可能依赖关系在发出指令时是未知的(开始执行)
处理器不使用在发布时检测内存依赖性,而是使用称为加载存储队列的结构来跟踪它们。此结构的作用是跟踪已发布但尚未停用的指令的挂起加载和存储的地址。如果存在内存订购违规,则可以检测到这种情况,并且可以从发生违规的位置重新开始执行。
回到伪代码示例,您可以想象在LD
之前执行ST
的情况(可能由于某些原因,R1中所需的值尚未准备好)。但是当ST
执行时,它会看到地址A
和B
相同。所以LD
应该真正读取ST
生成的值,而不是缓存中已经存在的陈旧值。因此,需要重新执行LD
以及LD
之后的任何说明。可以通过各种优化来减少一些开销,但基本思想仍然存在。
正如我前面提到的,检测这种依赖性的逻辑存在于允许推测执行内存指令(包括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内存模型对于硬件设计人员的潜在优势似乎不值得花费软件复杂性并增加额外的开销,需要更多的内存障碍。