正如 @Mackie 所说,管道将填充
cmp
。当另一个核心写入时,Intel 将不得不刷新这些
cmp
,这是一项昂贵的操作。如果 CPU 不刷新它,则会出现内存顺序违规。以下是此类违规的示例:
(此处开始 lock1 = lock2 = lock3 = var = 1)
线程1:
spin:
cmp lock1, 0
jne spin
cmp lock3, 0
je end
mov var, 0
end:
第二个线程:
mov lock3, 0
mov lock1, 0
mov ebx, var # I should know that var is 1 here.
首先,考虑线程1:
如果cmp lock1, 0; jne spin
分支预测lock1不为零,则将cmp lock3, 0
添加到流水线中。
在流水线中,cmp lock3, 0
读取lock3并发现它等于1。
现在,假设线程1花费了很长时间,线程2开始快速运行:
lock3 = 0
lock1 = 0
现在,让我们回到线程1:
假设
cmp lock1, 0
最终读取了lock1,并发现lock1为0,对其分支预测能力感到满意。
这个命令被提交,没有刷新任何内容。正确的分支预测意味着不会刷新任何内容,即使有乱序读取也是如此,因为处理器推断出不存在内部依赖关系。在CPU的眼中,lock3与lock1无关,所以一切都是可以的。
现在,
cmp lock3, 0
正确读取到lock3等于1,被提交了。
je end
未被执行,
mov var, 0
被执行。
在线程3中,
ebx
等于0,这应该是不可能的。这是英特尔必须补偿的内存顺序违规问题。
现在,英特尔为避免这种无效行为所采取的解决方案是刷新。当Thread 2上运行
lock3 = 0
时,它强制Thread 1刷新使用lock3的指令。在这种情况下,刷新意味着Thread 1将不会向流水线添加指令,直到使用lock3的所有指令被提交。在Thread 1的
cmp lock3
可以提交之前,必须提交
cmp lock1
。当
cmp lock1
尝试提交时,它读到lock1实际上等于1,并且分支预测失败了。这导致
cmp
被抛弃。现在Thread 1已经被刷新了,
lock3
在Thread 1的缓存中的位置被设置为
0
,然后Thread 1继续执行(等待
lock1
)。现在,Thread 2得知所有其他核心都刷新了对
lock3
的使用并更新了它们的缓存,因此Thread 2继续执行(在此期间它将执行独立语句,但下一个指令是另一个写操作,因此它可能需要挂起,除非其他核心有一个队列来保持挂起的
lock1 = 0
写操作)。
整个过程很昂贵,因此需要PAUSE。PAUSE帮助Thread 1,使其能够立即从即将发生的分支错误中恢复,并且在正确分支之前不必刷新流水线。PAUSE同样有助于Thread 2,使其不必等待Thread 1的刷新 (如前所述,我不确定此实现细节,但如果Thread 2尝试写入由太多其他核心使用的锁,则Thread 2最终必须等待刷新)。
一个重要的认识是,尽管在我的例子中需要刷新,但在Mackie的例子中则不需要。然而,CPU无法知道(它根本不分析代码,除了检查连续语句依赖关系和分支预测缓存),因此CPU会像在我的例子中一样刷新访问lockvar
指令,在Mackie的例子中也是如此,以保证正确性。