Windows Sleep(0) 与 PAUSE 指令
让我引用英特尔 64 和 IA-32 架构优化参考手册。
在多线程实现中,线程同步和产生调度的流行构造
对另一个等待执行其任务的线程的量子是坐在一个循环中并发出 SLEEP(0)。
这些通常称为“睡眠循环”(参见示例 #1)。需要注意的是,一个 SwitchToThread 调用
也可以使用。 “睡眠循环”在锁定算法和线程池中很常见,因为线程是
等待工作。
这种坐在紧密循环中并使用参数 0 调用 Sleep() 服务的结构实际上是一个
有副作用的轮询循环:
- 每次调用 Sleep() 都会经历昂贵的上下文切换成本,可能是 10000 多个周期。
- 它还承受从 ring 3 到 ring 0 转换的成本,可能是 1000 多个周期。
- 当没有其他线程等待获得控制权时,此睡眠循环对操作系统起作用
作为一个需要 CPU 资源的高度活跃的任务,防止操作系统将 CPU 置于低功耗状态
状态。
示例 #1。未优化的睡眠循环
while(!acquire_lock())
{ Sleep( 0 ); }
do_work();
release_lock();
示例 #2。使用 PAUSE 的功耗友好睡眠循环
if (!acquire_lock())
{ /* Spin on pause max_spin_count times before backing off to sleep */
for(int j = 0; j < max_spin_count; ++j)
{ /* intrinsic for PAUSE instruction*/
_mm_pause();
if (read_volatile_lock())
{
if (acquire_lock()) goto PROTECTED_CODE;
}
}
/* Pause loop didn't work, sleep now */
Sleep(0);
goto ATTEMPT_AGAIN;
}
PROTECTED_CODE:
do_work();
release_lock();
示例 #2 展示了使用 PAUSE 指令使睡眠循环更节能的技术。
通过使用 PAUSE 指令减慢“旋转等待”,多线程软件获得:
- 通过促进等待任务从繁忙的等待中更轻松地获取资源来提高性能。
- 通过在旋转时使用更少的管道部分来节省电力。
- 消除了由
Sleep(0) 调用。
在一个案例研究中,这项技术实现了 4.3 倍的性能提升,这意味着处理器节能 21%,平台节能 13%。
Skylake 微架构中的暂停延迟
PAUSE 指令通常与在位于同一处理器内核的两个逻辑处理器上执行的软件线程一起使用,等待释放锁。如此短的等待循环往往会持续数十到数百个周期,因此在性能方面,在占用 CPU 的同时等待比屈服于操作系统更有利。当等待循环预计持续数千个周期或更长时间时,最好通过调用操作系统同步 API 函数之一(例如 Windows 操作系统上的 WaitForSingleObject)来让步给操作系统。
PAUSE 指令旨在:
- 暂时为同级逻辑处理器(准备好退出自旋循环的前进进程)提供有竞争力的共享硬件资源。在 Skylake 微架构中,兄弟逻辑处理器可以竞争共享的微架构资源有: (1) Decode ICache、LSD 和 IDQ 中有更多的前端插槽; (2) RS 中有更多的执行槽。
- 与在以下配置中执行等效的自旋循环指令序列相比,节省处理器内核消耗的功率: (1) 一个逻辑处理器处于非活动状态(例如进入 C 状态); (2) 同一个核中的两个逻辑处理器都执行PAUSE指令; (3) HT 被禁用(例如使用 BIOS 选项)。
前一代微架构中 PAUSE 指令的延迟大约为 10 个周期,而在 Skylake 微架构上它已扩展到多达 140 个周期。
增加的延迟(允许更有效地利用竞争共享的微架构资源来准备好向前推进的逻辑处理器)对高线程应用程序有 1-2% 的小幅积极性能影响。如果在执行固定数量的循环 PAUSE 指令时未阻止前进进度,则预计对线程较少的应用程序的影响可以忽略不计。
在 2 核和 4 核系统中还有一点功率优势。由于 PAUSE 延迟显着增加,对 PAUSE 延迟敏感的工作负载将遭受一些性能损失。
您可以在“英特尔 64 和 IA-32 架构优化参考手册”和“英特尔 64 和 IA-32 架构软件开发人员手册”以及代码示例中找到有关此问题的更多信息。
我的意见
最好使程序逻辑以这样一种方式流动,即不需要 Sleep(0) 和 PAUSE 指令。换句话说,完全避免“旋转等待”循环。相反,请使用高级同步函数,如 WaitForMultipleObjects()、SetEvent() 等。这种高级同步函数是编写程序的最佳方式。如果您从性能、效率和节能方面分析可用的工具(随您的意愿) - 更高级别的功能是最佳选择。尽管它们也遭受昂贵的上下文切换和环 3 到环 0 的转换,但与您在所有“自旋等待”暂停周期或周期的总成本相比,这些费用并不常见,而且非常合理与睡眠(0)。
在支持超线程的处理器上,“自旋等待”循环会消耗处理器执行带宽的很大一部分。一个执行自旋等待循环的逻辑处理器会严重影响另一个逻辑处理器的性能。这就是为什么有时禁用超线程可能会提高性能,正如一些人所指出的那样。
在程序逻辑工作流中持续轮询设备或文件或状态更改会导致计算机消耗更多电量,给内存和总线带来压力,并提供不必要的页面错误(使用 Windows 中的任务管理器查看哪个应用程序在空闲状态下产生大多数页面错误,在后台等待用户输入 - 这些是最低效的应用程序,因为它们使用上面提到的轮询)。尽可能减少轮询(包括自旋循环),并使用事件驱动的意识形态和/或框架(如果可用)——这是我强烈推荐的最佳实践。您的应用程序应该一直处于睡眠状态,等待预先设置的多个事件。
事件驱动应用程序的一个很好的例子是 Nginx,最初是为类 unix 操作系统编写的。由于操作系统提供了各种功能和方法来通知您的应用程序,因此请使用这些通知而不是轮询设备状态更改。只需让您的程序无限休眠,直到通知到达或用户输入到达。使用这种技术可以减少代码轮询数据源状态的开销,因为当状态发生变化时,代码可以异步获取通知。