Parallel processing “的目的是什么?”;暂停“;x86中的指令?

Parallel processing “的目的是什么?”;暂停“;x86中的指令?,parallel-processing,x86,x86-64,intel,critical-section,Parallel Processing,X86,X86 64,Intel,Critical Section,我正在尝试创建一个愚蠢的自旋锁版本。浏览网页时,我发现x86中有一条名为“PAUSE”的汇编指令,用于向处理器提示此CPU上当前正在运行自旋锁。“英特尔手册”和其他可用信息说明 处理器使用此提示来避免内存顺序冲突 大多数情况下,这大大提高了处理器性能。对于 因此,建议在中放置暂停指令 所有旋转等待循环。文档中还提到“等待(一些 延迟)“是指令的伪实现 以上段落的最后一行是直观的。如果我抓不到锁,我必须等一段时间再抓锁 然而,在自旋锁的情况下,我们所说的内存顺序冲突是什么意思? “内存顺序冲突”是

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

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

以上段落的最后一行是直观的。如果我抓不到锁,我必须等一段时间再抓锁

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


以前曾在堆栈溢出时提出过自旋锁问题,但内存顺序冲突问题仍然没有得到回答(至少对我的理解是这样)。

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

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之前不太可能发生外部写入(即时间窗口更短)


当然,“去管道化”也会在自旋锁中浪费更少的能量,在超线程的情况下,它不会浪费其他线程可以更好地使用的资源。另一方面,在每个循环退出之前,仍有一个分支错误预测等待发生。英特尔的文档并不建议暂停消除管道刷新,但谁知道…

正如@Mackie所说,管道将填充
cmp
s。当另一个内核写入时,英特尔将不得不刷新这些
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:
线程2:

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

首先,考虑线程1:

如果
cmp锁1,则为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。这本来是不可能的。这是英特尔必须补偿的内存顺序冲突


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

整个过程代价高昂,因此需要暂停