当前位置: 首页 > 知识库问答 >
问题:

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

申博厚
2023-03-14

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

在大多数情况下,处理器使用此提示来避免内存顺序冲突,这大大提高了处理器性能。因此,建议在所有自旋等待循环中放置暂停指令。文档还提到“等待(一些延迟)”是指令的伪实现。

上段最后一行很直观,如果我抢锁不成功,我必须等一段时间再重新抢锁。

然而,在自旋锁的情况下,我们所说的内存顺序冲突是什么意思?“内存顺序冲突”是否意味着自旋锁后指令的错误推测加载/存储?

以前有人问过关于堆栈溢出的自旋锁问题,但内存顺序冲突问题仍然没有得到回答(至少就我的理解而言)。

共有2个答案

卢嘉誉
2023-03-14

正如@Mackie所说,管道将充满cmp。当另一个内核写入时,Intel将不得不刷新这些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:

Thread2:

mov lock3, 0
mov lock1, 0
mov ebx, var # I should know that var is 1 here.

首先,考虑线程1:

ifcmp lock1,0; jne旋转分支预测lock1不是零,它将cmp lock3,0添加到管道中。

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

现在,假设线程1正在享受它的甜蜜时间,并且线程2开始快速运行:

lock3 = 0
lock1 = 0

现在,让我们回到线程1:

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

此命令提交,不会刷新任何内容。正确的分支预测意味着不会刷新任何内容,即使是无序读取,因为处理器推断不存在内部依赖关系。在CPU看来,锁3并不依赖于锁1,所以这一切都没问题。

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

不取je end,执行mov var,0。

在线程3中,ebx等于0。这本来是不可能的。这是英特尔必须补偿的内存顺序冲突。

现在,英特尔为避免这种无效行为而采取的解决方案是刷新。当lock3=0在线程2上运行时,它会强制线程1刷新使用lock3的指令。在这种情况下,刷新意味着线程1不会将指令添加到管道中,直到所有使用lock3的指令都已提交。在线程1的cmp 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的指令,以保证正确性。

金晨
2023-03-14

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

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消除管道刷新,但谁知道呢...

 类似资料:
  • 我读过,x86的INC指令不是原子指令。我的问题是为什么会这样?假设我们在x86-64上递增一个64位整数,我们可以用一条指令来递增,因为INC指令同时处理内存变量和寄存器。那么为什么它不是原子的呢?

  • 我参考了下面的oracle jvm文档 https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.pop 弹出操作弹出操作数堆栈顶部的值 总体安排 pop表格pop=87(0x57) 操作堆栈...,值→ ... 说明从操作数堆栈中弹出顶部值。 除非值是1类计算类型的值(§2.11.1),否则不得使用pop指令。

  • 因此,我已经阅读了大约半年的关于x86处理器内部发生的事情。所以我决定尝试一下x86程序集的乐趣,只从80386指令开始,以保持它的简单性。(我主要是在学习,而不是优化) 几个月前我做了一个用C语言编写的游戏,所以我去那里用汇编代码从头重写了位图blitting函数。我不明白的是,循环的主要像素绘制主体使用C代码(18条指令)比我的汇编代码(只有7条指令,我几乎100%确定它不会跨越缓存行边界)更

  • 问题内容: 我正在将Linux与x86(准确地说是64位)配合使用。有没有一种方法可以获取当前指令的地址。实际上,我想编写自己的简化版本 setjmp /longjmp 。,R ..发布了 longjmp 的简化版本。任何想法如何实现 setjmp 。简化版本,不考虑异常和信号等。 问题答案: 我相信只要使用64位代码即可。 32位习惯用法是:

  • 本文向大家介绍什么是JSP指令?,包括了什么是JSP指令?的使用技巧和注意事项,需要的朋友参考一下 JSP指令影响Servlet类的整体结构。它通常具有以下形式- 指令标记有三种类型- 序号 指令与说明 1 <%@ page ...%> 定义与页面相关的属性,例如脚本语言,错误页面和缓冲要求。 2 <%@ include ...%> 在翻译阶段包含文件。 3 <%@ taglib ...%> 声明

  • 问题内容: 我花了很多时间阅读AngularJS文档和一些教程,对于文档的难懂性,我感到很惊讶。 我有一个简单的,可以回答的问题,对于其他希望使用AngularJS的人也可能有用: 什么是AngularJS指令? 某个地方应该有一个简单,精确的指令定义,但是AngularJS网站提供了这些令人惊讶的无用定义: 在主页上: 指令是AngularJS中可用的独特而强大的功能。指令可让您发明特定于您的应