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

what is the different of busy loop with Sleep(0) and pause instruction?

本文关键字:循环 什么 指令 暂停 睡眠      更新时间:2023-10-16

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

我看不出以下程序的cpu利用率有任何差异。我的问题不是关于节能方面的考虑。

#include <iostream>
using namespace std;
#include <windows.h>
bool t = false;
int main() {
while(t == false)
{
__asm { pause } ;
//Sleep(0);
}
}

Windows睡眠(0)与暂停指令

让我引用"英特尔64与IA-32体系结构优化参考手册"中的内容。

在多线程实现中,线程同步和生成调度中的一种流行结构另一个等待执行其任务的线程的quanta是坐在循环中并发出SLEEP(0)。

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

这种紧密循环并使用参数0调用Sleep()服务的构造实际上是带有副作用的轮询循环:

  • 每次对Sleep()的调用都会经历昂贵的上下文切换成本,这可能是1000+个周期
  • 它还承受着环3到环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指令减缓"旋转等待",多线程软件获得:

  • 通过方便等待任务从繁忙的等待中更容易地获取资源来实现性能
  • 通过在旋转时使用较少的管道部件来节省电力
  • 消除了由睡眠(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芯系统也有较小的功率优势。由于暂停延迟显著增加,对暂停延迟敏感的工作负载将遭受一些性能损失。

有关此问题的详细信息,请参阅"英特尔64及IA-32体系结构优化参考手册"answers"英特尔64与IA-32体系结构软件开发人员手册"以及代码示例。

我的看法

最好使程序逻辑以这样一种方式流动,即永远不需要Sleep(0)或PAUSE指令。换句话说,完全避免"旋转等待"循环。相反,使用高级同步函数,如WaitForMultipleObjects()SetEvent()等。这样的高级同步函数是编写程序的最佳方式。如果你从性能、效率和节能的角度分析可用的工具(根据你的意愿),那么更高级别的功能是最好的选择。尽管它们也会受到昂贵的上下文切换和从环3到环0的转换的影响,但与所有"旋转等待"PAUSE周期加在一起或使用Sleep(0)的周期相比,这些费用并不常见,而且非常合理。

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

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

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

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

__asm {pause};不可移植。

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