睡眠(0)和暂停指令的繁忙循环有什么不同?

sra*_*mij 6 c++ x86 hardware-interface busy-loop lockless

我想在我的应用程序中等待一个应该立即发生的事件,所以我不想让我的线程等待并稍后唤醒它。我想知道使用Sleep(0)和硬件暂停指令有什么区别。

我看不到以下程序的 CPU 利用率有任何差异。我的问题不是关于省电的考虑。

#include <iostream>
using namespace std;
#include <windows.h>

bool t = false;
int main() {
       while(t == false)
       {
              __asm { pause } ;
              //Sleep(0);
       }
}
Run Code Online (Sandbox Code Playgroud)

Max*_*tin 5

Windows Sleep(0) 与 PAUSE 指令

让我引用英特尔 64 位和 IA-32 架构优化参考手册。

在多线程实现中,线程同步和将调度量交给另一个等待执行其任务的线程的流行构造是处于循环中并发出 SLEEP(0)。

这些通常称为“睡眠循环”(参见示例 #1)。应该注意,也可以使用 SwitchToThread 调用。“睡眠循环”在锁定算法和线程池中很常见,因为线程正在等待工作。

这种坐在紧密循环中并以 0 参数调用 Sleep() 服务的构造实际上是一个具有副作用的轮询循环:

  • 每次调用 Sleep() 都会经历上下文切换的昂贵成本,可能是10000+ 个周期
  • 它还遭受环 3 到环 0 转换的成本,可能是1000+ 个周期
  • 当没有其他线程等待获得控制权时,此睡眠循环对操作系统表现为一个需要 CPU 资源的高度活跃的任务,从而防止操作系统将 CPU 置于低功耗状态。

示例#1。未优化的睡眠循环

while(!acquire_lock())
{ Sleep( 0 ); }
do_work();
release_lock();
Run Code Online (Sandbox Code Playgroud)

示例#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();
Run Code Online (Sandbox Code Playgroud)

Example #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 延迟敏感的工作负载将遭受一些性能损失。

您可以在“Intel 64 and IA-32 Architectures Optimization Reference Manual”和“Intel 64 and IA-32 Architectures Software Developer's Manual”以及代码示例中找到有关此问题的更多信息。

我的看法

最好使程序逻辑流向既不需要 Sleep(0) 也不需要 PAUSE 指令。换句话说,完全避免“自旋-等待”循环。相反,请使用高级同步函数,如WaitForMultipleObjects()SetEvent()等。这种高级同步函数是编写程序的最佳方式。如果您从性能、效率和节能方面分析可用工具(根据您的意愿) - 更高级别的功能是最佳选择。尽管它们也遭受昂贵的上下文切换和环 3 到环 0 的转换,但与您为所有“旋转等待”暂停周期组合或周期的总花费相比,这些费用很少见并且是合理的与睡眠(0)。

在支持超线程的处理器上,“自旋-等待”循环会消耗处理器执行带宽的很大一部分。一个执行自旋等待循环的逻辑处理器会严重影响另一个逻辑处理器的性能。这就是为什么有时禁用超线程可能会提高性能,正如某些人所指出的那样。

在程序逻辑工作流中持续轮询设备或文件或状态更改会导致计算机消耗更多电量,对内存和总线施加压力并提供不必要的页面错误(使用 Windows 中的任务管理器查看哪些应用程序产生最多处于空闲状态时的页面错误,在后台等待用户输入 - 这些是效率最低的应用程序,因为它们正在使用上述轮询)。尽可能减少轮询(包括自旋循环)并使用事件驱动的意识形态和/或框架(如果可用) - 这是我强烈推荐的最佳实践。您的应用程序应该一直处于睡眠状态,等待预先设置的多个事件。

事件驱动应用程序的一个很好的例子是 Nginx,它最初是为类 Unix 操作系统编写的。由于操作系统提供了各种函数和方法来通知您的应用程序,因此请使用这些通知而不是轮询设备状态更改。只需让您的程序无限休眠,直到通知到达或用户输入到达。使用这种技术可以减少代码轮询数据源状态的开销,因为代码可以在发生状态更改时异步获取通知。


lit*_*adv -1

Sleep是一个系统调用,它允许操作系统在允许调用者继续之前将 CPU 时间重新安排给任何其他进程(如果可用)(即使参数为 0)。

__asm {pause};不便于携带。

嗯,Sleep两者都不是,但不是在 CPU 级别,而是在系统库级别。