什么是x86的“暂停”指令的目的是什么?(What is the purpose of the “P

2019-06-18 13:14发布

我想创建一个自旋锁的哑版本。 浏览网页时,我遇到了在86这是用来给提示,一个自旋锁是目前该CPU上运行的处理器称为“PAUSE”一个汇编指令。 英特尔手册和其他资料状态

处理器使用该提示,以避免存储器顺序违反在大多数情况下,大大提高处理器的性能。 出于这个原因,建议到暂停指令被放置在所有的自旋等待循环。 该文件还提到,“等待(一些延迟)”是指令的执行伪。

上述段落的最后一行是直观的。 如果我在抓住锁定不成功,我必须再次抓住锁之前等待一段时间。

然而,我们需要什么样自旋锁的情况下是指由内存顺序违规? 是否“记忆顺序违反”平均自旋锁后,指示不正确的推测性加载/存储?

自旋锁问题已经被问关于堆栈溢出之前,但记忆顺序违规问题仍然没有答案(AT-至少在我的理解)。

Answer 1:

试想,处理器将如何执行通常的旋等待循环:

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)(也投机)。 这是当存储顺序违规行为,会。 每当处理器“看到”外部写(从另一个处理器写),它会搜索其管道为它推测访问相同的内存位置,也没有提交指令。 如果找到任何这样的指令,则处理器的推测状态是无效的,并与管线冲洗擦除。

不幸的是这种情况会(很可能)重复每一个处理器上的自旋锁的等待时间,使这些锁定比他们应该要慢得多。

进入暂停指令:

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之前被提交。

当然,“去流水线”也将在自旋锁和超线程不会浪费资源,其他线程可以使用更好的情况下浪费更少的能源。 在另一方面还有一个分支误预测等待每个循环退出之前发生。 英特尔的文档并不意味着暂停消除了管道冲洗,但谁知道...



Answer 2:

作为@Mackie说,该管道将与填充cmp秒。 英特尔将对冲洗那些cmp ■当另一个核心写入,这是一种昂贵的操作。 如果CPU不刷新它,那么你有一个内存顺序冲突。 这样的违反的一个例子是下面的:

(这将启动与锁1 =锁2 = 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 cmp lock1, 0; jne spin分支预测,锁1不为零,它增加了cmp lock3, 0到管道。

在管道, cmp lock3, 0读取lock3并找出它等于1。

现在,假设线程1正在它的甜蜜时光,和线程2开始快速运行:

lock3 = 0
lock1 = 0

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

比方说, cmp lock1, 0最后读锁1,发现该锁1为0,并且是高兴的分支预测能力。

此命令提交,并没有什么被刷新。 正确的分支预测意味着什么被刷新,甚至乱序读取,因为处理器推断,没有内部的依赖。 lock3是不依赖于CPU的目光锁1,所以这一切都是好的。

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

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

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


现在,英特尔需要以避免无效的问题的解决方案,是冲洗。 当lock3 = 0上线程2运行,它迫使线程1冲洗使用lock3指令。 在这种情况下冲洗意味着直到使用lock3所有指令一直致力于线程1不会增加指令流水线。 线程1的前cmp lock3可以承诺,在cmp lock1必须承诺。 当cmp lock1尝试提交,它读取锁1实际上等于1,并且该分支预测失败了。 这将导致cmp得到抛出。 现在线程1被刷新, lock3在线程1的高速缓存中的位置被设置为0 ,然后线程1继续执行(上等待lock1 )。 线程2现在就通知所有其他核都已刷新的使用lock3和更新了自己的高速缓存,所以线程2则继续执行(这将已经执行在此期间独立声明,但下一条指令是另一个写操作,这样它可能有挂,除非其他核有一个队列来保存未决的lock1 = 0写入)。

这整个过程是昂贵的,因此PAUSE。 暂停帮助了线程1,可以从现在即将分支错误预测立即恢复,而且它没有正确的分支之前刷新其管道。 暂停同样帮助了线程2,它不必等待线程1的冲洗(就像之前说的,我不能确定这个实现的细节,但是,如果线程2次尝试写锁使用太多其他内核,线程2将最终要等待刷新)。

一个重要的认识是,虽然在我的例子,需要冲洗,在麦基的例子,事实并非如此。 然而,CPU没有办法知道(它不分析代码在所有的,除了检查连续语句的依赖,和分支预测缓存),因此CPU将刷新访问指令lockvar在麦基的例子,是因为它在我的为了保证正确性。



文章来源: What is the purpose of the “PAUSE” instruction in x86?