基于暂停的自旋等待循环
正如我从您的问题中了解到的那样,您的案件的等待时间很长。在这种情况下,根本不推荐使用自旋等待循环。但是,如果您使用的是不断检查内存中的值的自旋循环(例如,字节大小的同步变量),请使用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 处理器(2017 年 1 月发布,基于 Kaby-Lake-S 微架构)上 500 个 CPU 周期是 0.0000001 秒,即 1/10000000 秒:CPU 每秒可以进行 1000 万次这 500 -CPU 周期循环。
英特尔推荐的这个 500 周期限制是理论上的,并且完全取决于特定的用例,即需要通过自旋等待循环同步的代码逻辑。根据基准测试,某些场景(例如Delphi 的 FastMM4-AVX 内存管理器)在值为 5000 时效果更好。尽管如此,这些基准并不总是反映真实世界的场景,应该测量真实的程序用例。
如您所见,这个PAUSE
基于自旋等待的循环的时间非常短。
另一方面,对像 Sleep() 这样的 API 函数的每次调用都会经历昂贵的上下文切换成本,可能是 10000 多个周期;它还承受从环 3 到环 0 转换的成本,可能是 1000 多个周期。
如果有更多线程,则处理器内核(如果存在,则乘以超线程功能)可用,并且一个线程将在临界区中间切换到另一个线程,等待另一个线程的临界区可能真的需要很长时间,至少 10000+ 个周期,因此PAUSE
基于 - 的自旋等待循环将是徒劳的。
除了英特尔优化参考手册的相关章节,更多信息请参见以下文章:
当等待循环预计持续数千个周期或更多时,最好通过调用操作系统同步 API 函数之一来让步给操作系统,例如Windows 操作系统上的WaitForSingleObject
或SwitchToThread
。
作为结论:在您的场景中,PAUSE
基于 - 的 spin-wait 循环不会是最佳选择,因为您的等待时间很长,而 spin-wait 循环适用于非常短的循环。
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 个周期。在 2013 年 6 月发布的 Intel Core i5-4430 (3GHz) 上为 2.81ns。因此,对于长循环,最好使用 OS 同步 API 函数将控制权交给其他线程,而不是用PAUSE
循环占用 CPU,而不管微架构。
测试,测试和设置
请注意,旋转等待循环也必须正确实施。英特尔推荐使用所谓的“测试、测试和设置”技术(参见英特尔 64 和 IA-32 架构优化参考手册的第 11.4.3 节“使用自旋锁进行优化”)来确定同步变量的可用性. 根据这种技术,第一个“测试”是通过正常(非锁定)内存负载完成的,以防止在自旋等待循环期间过度的总线锁定;xchg
如果变量在第一步(“测试”)的非锁定内存加载时可用,则继续通过总线锁定原子指令完成的第二步(“测试和设置”) 。
但请注意,与仅单步“测试和设置”相比,在“测试和设置”之前使用“测试”的这种两步方法可能会增加非竞争案例的成本。最初的只读访问可能只会获得处于共享状态的缓存行,因此像 test-and-set ( xchg
) 或 compare-and-swap ( cmpxchg
) 这样的原子操作仍然需要“Read For Ownership” (RFO) 操作获得缓存行的独占所有权。此操作由试图写入处于共享状态的高速缓存行的处理器发出。