我正在尝试创建一个自旋锁的哑版 . 浏览网页时,我在x86中遇到了一个名为“PAUSE”的汇编指令,该指令用于向处理器提供当前在此CPU上运行自旋锁的提示 . 英特尔手册和其他可用信息说明了这一点
在大多数情况下,处理器使用此提示来避免内存顺序违规,从而大大提高了处理器性能 . 因此,建议在所有自旋等待循环中放置PAUSE指令 . 文档还提到“等待(一些延迟)”是指令的伪实现 .
上段的最后一行很直观 . 如果我没有成功 grab 锁,我必须等待一段时间然后再 grab 锁 .
但是,在旋转锁定的情况下,内存顺序违规是什么意思? "memory order violation"是指旋转锁定后指令的错误推测加载/存储吗?
关于堆栈溢出之前已经询问了自旋锁定问题但是内存顺序违规问题仍未得到解决(至少对于我的理解) .
2 回答
试想一下,处理器如何执行典型的自旋等待循环:
在几次迭代之后,分支预测器将预测将永远不会采用条件分支(3)并且管道将填充CMP指令(2) . 这一直持续到最后另一个处理器将零写入lockvar . 此时,我们的管道中充满了推测(即尚未提交)的CMP指令,其中一些已经读取了lockvar,并向下面的条件分支(3)报告了(不正确的)非零结果(也是推测性的) . 这是发生内存顺序违规的时候 . 每当处理器“看到”外部写入(来自另一个处理器的写入)时,它就在其管道中搜索推测性地访问相同存储器位置但尚未提交的指令 . 如果找到任何这样的指令,则处理器的推测状态是无效的并且用流水线刷新擦除 .
不幸的是,这种情况(非常可能)每次处理器等待自旋锁时都会重复,并使这些锁比它们应该的速度慢得多 .
输入PAUSE指令:
PAUSE指令将“解压缩”内存读取,因此管道中没有像第一个示例中那样填充推测CMP(2)指令 . (即它可以阻塞管道直到所有旧的存储器指令都被提交 . )因为CMP指令(2)顺序执行,所以在CMP指令(2)读取之后发生外部写操作不太可能(即时间窗口要短得多) lockvar但在提交CMP之前 .
当然,“去流水线”也会在自旋锁中浪费更少的能量,并且在超线程的情况下,它不会浪费其他线程可以更好地使用的资源 . 另一方面,在每个循环退出之前仍然存在等待发生的分支错误预测 . 英特尔的文档并未暗示PAUSE消除了管道冲洗,但谁知道......
正如@Mackie所说,管道将填充
cmp
. 当另一个核心写入时,英特尔将不得不冲洗那些cmp
,这是一项昂贵的操作 . 如果CPU没有刷新它,那么您的内存订单违规 . 这种违规行为的一个例子如下:(以lock1 = lock2 = lock3 = var = 1开头)
线程1:
线程2:
首先,考虑线程1:
如果
cmp lock1, 0; jne spin
branch预测lock1不为零,则将cmp lock3, 0
添加到管道 .在管道中,
cmp lock3, 0
读取lock3并发现它等于1 .现在,假设线程1正在消耗它的甜蜜时间,并且线程2开始快速运行:
现在,让我们回到主题1:
假设
cmp lock1, 0
最终读取lock1,发现lock1为0,并对其分支预测能力感到高兴 .此命令提交,没有任何内容被刷新 . 正确的分支预测意味着即使没有顺序读取也不会刷新任何内容,因为处理器推断出没有内部依赖性 . lock3并不依赖于CPU眼中的lock1,所以这一切都没问题 .
现在,正确读取lock3的
cmp lock3, 0
等于1,提交 .je end
未被执行,mov var, 0
执行 .在线程3中,
ebx
等于0.这应该是不可能的 . 这是英特尔必须违反的内存顺序补偿 .现在,英特尔为避免这种无效行为而采取的解决方案是冲洗 . 当
lock3 = 0
在线程2上运行时,它会强制线程1刷新使用lock3的指令 . 在这种情况下刷新意味着在所有使用lock3的指令都已提交之前,线程1不会向管道添加指令 . 在线程1的cmp lock3
可以提交之前,cmp lock1
必须提交 . 当cmp lock1
尝试提交时,它会读取lock1实际上等于1,并且分支预测是失败的 . 这会导致cmp
被抛出 . 现在刷新了线程1,lock3
's location in Thread 1'的缓存设置为0
,然后线程1继续执行(等待lock1
) . 线程2现在得到通知,所有其他核心已刷新lock3
的使用并更新了它们的缓存,因此线程2然后继续执行(它将同时执行独立语句,但下一条指令是另一个写入因此它可能必须挂起,除非其他核心有一个队列来保持挂起的lock1 = 0
写入 .整个过程很昂贵,因此PAUSE . PAUSE帮助线程1,它现在可以立即从即将发生的分支错误预测中恢复,并且在正确分支之前不必刷新其管道 . PAUSE同样帮助线程2,它不必等待线程1的刷新(如前所述,我不确定这个实现细节,但如果线程2尝试写入太多其他内核使用的锁,则线程2将最终不得不等待冲洗) .
一个重要的理解是,在我的例子中,需要刷新,在Mackie 's example, it is not. However, the CPU has no way to know (It doesn't分析代码,除了检查连续语句依赖性和分支预测缓存之外),因此CPU将刷新访问
lockvar
的指令,只是在Mackie的例子中和我一样,为了保证正确性 .