x86中"PAUSE"指令的目的是什么?

pra*_*kar 53 parallel-processing x86 x86-64 intel critical-section

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

在大多数情况下,处理器使用此提示来避免内存顺序违规,从而大大提高了处理器性能.因此,建议在所有自旋等待循环中放置PAUSE指令.文档还提到"等待(一些延迟)"是指令的伪实现.

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

但是,在旋转锁定的情况下,内存顺序违规是什么意思?"内存顺序违规"是否意味着旋转锁定后指令的错误推测加载/存储?

关于堆栈溢出之前已经询问了自旋锁定问题但是内存顺序违规问题仍未得到解决(至少对于我的理解).

Mac*_*ser 82

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

1 Spin_Lock:
2    CMP lockvar, 0   ; Check if lock is free
3    JE Get_Lock
4    JMP Spin_Lock
5 Get_Lock:
Run Code Online (Sandbox Code Playgroud)

在几次迭代之后,分支预测器将预测将永远不会采用条件分支(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:
Run Code Online (Sandbox Code Playgroud)

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

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


Nic*_*one 6

正如@Mackie 所说,管道将填充cmps。英特尔将不得不冲洗那些cmp当另一个内核写入时,s,这是一项昂贵的操作。如果 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:
Run Code Online (Sandbox Code Playgroud)

主题 2:

mov lock3, 0
mov lock1, 0
mov ebx, var # I should know that var is 1 here.
Run Code Online (Sandbox Code Playgroud)

首先,考虑线程 1:

如果cmp lock1, 0; jne spin分支预测 lock1 不为零,则将其添加cmp lock3, 0到管道中。

在管道中,cmp lock3, 0读取 lock3 并发现它等于 1。

现在,假设线程 1 正在度过它的美好时光,而线程 2 开始快速运行:

lock3 = 0
lock1 = 0
Run Code Online (Sandbox Code Playgroud)

现在,让我们回到主题 1:

假设cmp lock1, 0最终读取 lock1,发现 lock1 为 0,并且对其分支预测能力感到高兴。

此命令会提交,并且不会刷新任何内容。正确的分支预测意味着没有任何东西被刷新,即使是乱序读取,因为处理器推断出没有内部依赖性。在 CPU 的眼中,lock3 不依赖于 lock1,所以这一切都没问题。

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

je end不被采取,并mov var, 0执行。

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


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

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

一个重要的理解是,虽然在我的例子中,冲洗是必需的,但在 Mackie 的例子中,它不是。然而,CPU 没有办法知道(它根本不分析代码,除了检查连续的语句依赖性和分支预测缓存),所以 CPU 会刷新lockvar在 Mackie 的例子中访问的指令,就像它在我的例子中一样,以保证正确性。

  • Mackie 的答案的主要问题(我认为)是所有负载都位于同一位置并且属于同一指令。所以实际上一开始就不会有重新排序。有两个不同的负载是一个现实的例子。 (2认同)