x86暂停指令在自旋锁中是如何工作的,它能在其他情况下使用吗?

52

pause指令通常用于测试自旋锁的循环中,当其他一些线程拥有自旋锁时,以减轻紧密循环的压力。据说它等效于一些NOP指令。请问有人能告诉我它如何确切地用于自旋锁优化吗?对我来说,即使是NOP指令也会浪费CPU时间。它们会降低CPU使用率吗?

另一个问题是,我是否可以将暂停指令用于其他类似目的。例如,我有一个繁忙的线程,它不断扫描某些地方(例如队列)以检索新节点; 但是,有时队列为空,线程只是浪费cpu时间。让线程睡眠并由其他线程唤醒可能是一种选择,但该线程很关键,所以我不想让它睡眠。

暂停指令能否用于我的目的,以减轻CPU使用情况?目前它使用了一个物理内核的100%CPU?


自旋等待的目的就是浪费 CPU 时间。烧掉 100% 的核心对性能来说是不好的,它会阻止另一个线程给你的工作线程分配任务。 - Hans Passant
4个回答

40

PAUSE 告知 CPU 这是一个自旋锁等待循环,以便优化内存和缓存访问。请参阅 x86 的 PAUSE 指令,了解更多有关离开自旋循环时避免内存顺序误判的详细信息。

PAUSE 可能会实际上停止 CPU 一段时间以节省能源。旧版 CPU 将其解码为 REP NOP,所以您不必检查是否支持。旧版 CPU 将尽可能快地执行无操作(NOP)。

另请参阅 https://software.intel.com/en-us/articles/benefitting-power-and-performance-sleep-loops


更新:我认为在队列检查中使用 PAUSE 不是一个好主意,除非您要将队列类似于自旋锁(并且没有明显的方法可以这样做)。

即使使用 PAUSE,长时间自旋仍然非常糟糕。


是的,我有这样一种印象,即PAUSE是一种专门的指令,因此不适合其他用途,可能是由于它的副作用。我对细节很感兴趣。也许英特尔手册是一个很好的参考:) - Infinite
7
顺便说一下,暂停(PAUSE)不会解决你的问题:它会停止CPU以节省电力,但不会为其他任务重新安排CPU。在PAUSE中挂起或进行忙碌循环没有区别,你仍然会得到100%的CPU负载,只是功耗更低。 - blaze
3
你提供的链接已经失效了。我建议你访问以下链接:https://software.intel.com/en-us/articles/benefitting-power-and-performance-sleep-loops。 - Zan Lynx
@ZanLynx:现在也已经死了。至少在这篇文章中有相关内容:https://www.intel.com/content/www/us/en/developer/articles/technical/a-common-construct-to-avoid-the-contention-of-threads-architecture-agnostic-spin-wait-loops.html - Bouncner

21
当处理器检测到可能存在内存顺序违规的情况时,退出循环会导致严重的性能损失。 PAUSE指令向处理器提供了一个提示,表明代码序列是自旋等待循环。 处理器使用此提示来在大多数情况下避免内存顺序违规,从而极大地提高了处理器性能。 因此,建议在所有自旋等待循环中放置PAUSE指令。 PAUSE指令的另一个功能是减少Intel处理器消耗的功率。 【来源:英特尔手册】

7

基于暂停的自旋等待循环

从您的问题中我了解到,在您的情况下,等待时间非常长。在这种情况下,不建议使用自旋等待循环。但是,如果您正在使用一个自旋循环来检查内存中的值(例如一个字节大小的同步变量),请使用PAUSE指令。请参阅Intel 64和IA-32体系结构优化参考手册第11.4.2节“短时间同步”。

您写道,您有一个“线程一直在扫描某些位置(例如队列)以检索新节点”的情况。

在这种情况下(即长时间等待),英特尔建议使用操作系统的同步API函数。例如,您可以在队列中出现新节点时创建一个事件,并使用WaitForSingleObject(Handle, INFINITE)等待此事件。每当出现新节点时,队列将触发此事件。

根据英特尔优化参考手册第2.3.4节“Skylake客户端微架构中的暂停延迟”,
PAUSE指令通常与在同一处理器核心中执行的两个逻辑处理器上运行的软件线程一起使用,等待锁定被释放。这种短暂的等待循环通常持续十几个到几百个周期,因此从性能上讲,最好占用CPU等待而不是让出给操作系统。
从上述引文中,“十几个到几百个周期”我理解为20到500个CPU周期。
在4500 MHz英特尔酷睿i7 7700K处理器(基于Kaby-Lake-S微架构于2017年1月发布)上,500个CPU周期相当于0.0000001秒,即1/10000000秒:CPU每秒可以执行1000万次这个500-CPU周期循环。
这个由英特尔推荐的500次循环限制是理论上的,一切取决于具体的使用情况,即需要通过自旋等待循环进行同步的代码逻辑。像Delphi的FastMM4-AVX内存管理器这样的一些场景在基准测试中使用5000的值效果更好。尽管如此,这些基准测试并不总是反映实际情况,应该测量真实的程序用例。
正如您所看到的,这个基于PAUSE的自旋等待循环是为了非常短的时间。
另一方面,每个调用API函数(如Sleep())都会经历昂贵的上下文切换成本,可能超过10000个周期;它还会遭受从第3到第0环的转换成本,可能超过1000个周期。
如果线程数超过处理器核心数(乘以超线程特性,如果有的话),并且一个线程在关键部分中被切换到另一个线程时,等待来自另一个线程的关键部分可能需要非常长的时间,至少需要10000个周期,因此基于PAUSE的自旋等待循环将是徒劳的。
除了英特尔优化参考手册的相关章节外,请参阅以下文章以获取更多信息: 当等待循环预计持续数千个周期或更长时间时,最好通过调用操作系统同步 API 函数之一(例如 Windows 操作系统上的 WaitForSingleObjectSwitchToThread)让操作系统进行调度。
总之,在您的场景中,基于PAUSE的自旋等待循环不是最佳选择,因为您的等待时间很长,而自旋等待循环是为非常短的循环而设计的。 PAUSE指令在基于Skylake微架构或更高版本的处理器上需要约140个CPU周期。例如,在2015年8月发布的Intel Core i7-6700K CPU(4GHz)上,它只需35.10ns,或者在2020年9月发布的移动设备Intel Core i7-1165G7 CPU上需要49.47ns。在早期处理器(Skylake之前),例如基于Haswell微架构的处理器上,它需要约9个周期。对于长循环,最好使用操作系统同步API函数将控制权交给其他线程,而不是占用CPU执行PAUSE循环,无论微架构如何。

测试、测试并设置

请注意,自旋等待循环也必须正确实现。英特尔推荐使用所谓的“测试、测试和设置”技术(请参见《英特尔64和IA-32体系结构优化参考手册》第11.4.3节“自旋锁优化”),以确定同步变量的可用性。根据这种技术,第一次“测试”是通过正常(非锁定)内存加载来完成的,以防止在自旋等待循环期间出现过多的总线锁定;如果在第一步(“测试”)的非锁定内存加载时变量可用,则继续进行第二步(“测试和设置”),该步骤通过总线锁定原子xchg指令完成。
但请注意,使用“测试”在“测试并设置”之前的这个两步方法可能会增加未竞争情况下的成本,与仅使用单步“测试并设置”相比。初始的只读访问可能仅获取共享状态的缓存行,因此像test-and-set (xchg)或compare-and-swap (cmpxchg)这样的原子操作仍需要进行“Read For Ownership” (RFO) 操作才能获得缓存行的独占所有权。 该操作由尝试写入处于共享状态的缓存行的处理器发出。

5
如果你说“旋转循环不好,因为等待时间太长”,会更好。说“pause无法帮助”是错误的:如果你设计程序得不好,而且旋转时间非常长,那么在这些循环中使用pause仍然有帮助。事实上,在这种情况下它甚至更加重要。(但对于同步来说,显然还是错误的选择。更好的做法是使用操作系统支持的旋转锁,它可以让出CPU并等待唤醒,如果不能快速获得锁的话。http://preshing.com/20111124/always-use-a-lightweight-mutex/.) - Peter Cordes
1
此外,即使没有超线程,pause 也有帮助:当退出自旋循环时,它可以避免在加载时出现内存顺序错误的停顿。https://dev59.com/R2cs5IYBdhLWcg3wYzBU - Peter Cordes
@PeterCordes 好的,我会重新措辞我的回答,我的意思是 EnterCriticalSection 比带有“暂停”的长循环更好。 - Maxim Masiutin
@PeterCordes 再次感谢您宝贵的建议和编辑。顺便说一下,链接到 https://preshing.com/20111124/always-use-a-lightweight-mutex/ 中描述的使用CriticalSection的轻量级互斥锁仅在未竞争的情况下(单线程)提供优势,就像该文章的示例一样。在高度竞争的情况下,EnterCriticalSection 在只有一个已锁定的“测试和设置”之后将继续进行昂贵的上下文切换(10000+),而基于PAUSE的自旋等待循环可能会在几次迭代后以较小的成本获得锁定,并且 'pause' 所花费的周期不会被浪费,而是被放弃。 - Maxim Masiutin
不,PAUSE并不消耗太多的周期 - 请查看agner.org上的延迟/吞吐量表。 - Bonita Montero
@BonitaMontero,您能否澄清一下:您所说的“such a lot of cycles”具体指什么? - Maxim Masiutin

1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接