x86中“PAUSE”指令的目的是什么?

时间:2012-10-15 10:52:44

标签: parallel-processing x86 x86-64 intel critical-section

我正在尝试创建一个自旋锁的哑版。浏览网页时,我在x86中遇到了一个名为“PAUSE”的汇编指令,该指令用于向处理器提供当前在此CPU上运行自旋锁的提示。英特尔手册和其他可用信息表明

  

处理器使用此提示来避免违反内存顺序   大多数情况,这大大提高了处理器性能。对于   这个原因,建议放入PAUSE指令   所有旋转等待循环。该文件还提到“等待(一些   延迟)“是指令的伪实现。

上一段的最后一行很直观。如果我没有成功抓住锁,我必须等待一段时间然后再抓住锁。

但是,在旋转锁定的情况下,内存顺序违规是什么意思? “内存顺序违规”是否意味着旋转锁定后指令的推测性加载/存储不正确?

之前已经在堆栈溢出问题上询问了自旋锁定问题,但是内存顺序违规问题仍未得到解答(至少对于我的理解而言)。

2 个答案:

答案 0 :(得分:73)

想象一下,处理器将如何执行典型的自旋等待循环:

1 Spin_Lock:
2    CMP lockvar, 0   ; Check if lock is free
3    JE Get_Lock
4    JMP Spin_Lock
5 Get_Lock:

在几次迭代之后,分支预测器将预测将永远不会采用条件分支(3)并且管道将填充CMP指令(2)。这一直持续到最后另一个处理器将零写入lockvar。此时,我们的管道中充满了推测(即尚未提交)的CMP指令,其中一些已经读取了lockvar,并向下面的条件分支(3)报告了(不正确的)非零结果(也是推测性的)。这是发生内存顺序违规的时候。每当处理器“看到”外部写入(来自另一个处理器的写入)时,它就在其管道中搜索推测性地访问相同存储器位置但尚未提交的指令。如果找到任何此类指令,则处理器的推测状态无效,并通过管道刷新擦除。

不幸的是,这种情况(非常可能)会在每次处理器等待自旋锁时重复,并使这些锁比它们应该的速度慢得多。

输入PAUSE指令:

1 Spin_Lock:
2    CMP lockvar, 0   ; Check if lock is free
3    JE Get_Lock
4    PAUSE            ; Wait for memory pipeline to become empty
5    JMP Spin_Lock
6 Get_Lock:

PAUSE指令将“解压缩”内存读取,因此管道未填充推测CMP(2)指令,如第一个示例中所示。 (即它可以阻塞管道直到所有旧的存储器指令都被提交。)因为CMP指令(2)顺序执行,所以在CMP指令(2)读取之后发生外部写操作不太可能(即时间窗口要短得多) lockvar但在提交CMP之前。

当然,“去流水线”也会在自旋锁中浪费更少的能量,在超线程的情况下,它不会浪费其他线程可以更好地使用的资源。另一方面,在每个循环退出之前仍然存在等待发生的分支错误预测。英特尔的文档并未暗示PAUSE消除了管道冲洗,但谁知道......

答案 1 :(得分:3)

正如@Mackie所说,管道将充满cmp。当另一个内核写入时,英特尔将不得不刷新这些cmp,这是一项昂贵的操作。如果CPU不刷新它,则表明存在内存顺序冲突。以下是此类违规的示例:

(以lock1 = lock2 = lock3 = var = 1开头)

线程1:

spin:
cmp lock1, 0
jne spin
cmp lock3, 0 # lock3 should be zero, Thread 2 already ran.
je end # Thus I take this path
mov var, 0 # And this is never run
end:

线程2:

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,所以这一切都可以。

现在,正确读取lock3等于1的cmp lock3, 0提交了。

je end不被采用,而mov var, 0被执行。

在线程3中,ebx等于0。这应该是不可能的。这是英特尔必须弥补的违反内存顺序的问题。


现在,英特尔为避免这种无效行为而采取的解决方案是冲洗。当lock3 = 0在线程2上运行时,它将强制线程1刷新使用lock3的指令。在这种情况下,刷新意味着线程1不会在提交所有使用lock3的指令之前将指令添加到管道中。必须先提交cmp lock3,然后才能提交线程1的cmp lock1。当cmp lock1尝试提交时,它会读取到lock1实际上等于1,并且分支预测失败。这导致cmp被抛出。现在刷新了线程1,将lock3在线程1的缓存中的位置设置为0,然后线程1继续执行(等待lock1)。现在通知线程2所有其他内核都刷新了lock3的用法并更新了其缓存,因此线程2然后继续执行(在此期间它将执行独立的语句,但是下一条指令是另一条写操作,因此可能必须挂起,除非其他内核有一个队列来保存挂起的lock1 = 0写操作。)

整个过程很昂贵,因此暂停。暂停有助于解决线程1的问题,线程1现在可以立即从即将发生的分支错误预测中恢复,并且不必在正确分支之前先冲洗其管道。暂停类似地帮助线程2,而不必等待线程1的刷新(如前所述,我不确定该实现细节,但是如果线程2尝试编写太多其他内核使用的锁,则线程2将最终不得不等待冲洗。

一个重要的理解是,尽管在我的示例中需要冲洗,但在Mackie的示例中却不需要冲洗。但是,CPU没有办法知道(除了检查连续的语句依赖项和分支预测缓存之外,它根本不分析代码),因此CPU将刷新Mackie示例中访问lockvar的指令为了确保正确性,它在我的网站上进行。