std::this_thread::sleep_for()和纳秒

19

如果我将两个调用放在一起来确定最小可测量的时间持续时间:

// g++ -std=c++11 -O3 -Wall test.cpp
#include <chrono>
typedef std::chrono::high_resolution_clock hrc;

hrc::time_point start = hrc::now();
hrc::time_point end   = hrc::now();
std::chrono::nanoseconds duration = end - start;
std::cout << "duration: " << duration.count() << " ns" << std::endl;

我已经在循环中运行了数千次,并且在我的特定3.40GHz台式机上始终获得40 ns +/- 2 ns。

但是,当我尝试短暂休眠时:

#include <thread>

hrc::time_point start = hrc::now();
std::this_thread::sleep_for( std::chrono::nanoseconds(1) );
hrc::time_point end   = hrc::now();
std::chrono::nanoseconds duration = end - start;
std::cout << "slept for: " << duration.count() << " ns" << std::endl;

这告诉我,我平均睡眠了55400纳秒,相当于55.4微秒。比我预期的时间要长得多。

将上述代码放入一个for()循环中,我尝试了不同的睡眠时间,这是结果:

  • sleep_for( 4000 ns ) => 睡眠了58000 ns
  • sleep_for( 3000 ns ) => 睡眠了57000 ns
  • sleep_for( 2000 ns ) => 睡眠了56000 ns
  • sleep_for( 1000 ns ) => 睡眠了55000 ns
  • sleep_for( 0 ns ) => 睡眠了54000 ns
  • sleep_for( -1000 ns ) => 睡眠了313 ns
  • sleep_for( -2000 ns ) => 睡眠了203 ns
  • sleep_for( -3000 ns ) => 睡眠了215 ns
  • sleep_for( -4000 ns ) => 睡眠了221 ns

我有几个问题:

  • 什么原因可以解释这些数字?
  • 为什么负数睡眠时间返回200+ ns,而0+纳秒的睡眠结果却是50,000 +纳秒?
  • 将负数作为睡眠时间是一项记录/支持的功能,还是我意外地遇到了一些奇怪的bug,我不能依赖它?
  • 有没有更好的C++睡眠调用可以给我更一致/可预测的睡眠时间?

1
这就是 nanosleep({0,1}, NULL) 所需的时间(如果你使用的是Linux)。 - Cubbi
我无法复制这些结果,sleep_for(1ns) 给我的是 0ns。 - Rapptz
2
sleep_for 会至少休眠指定的持续时间。如果您提供了一个负值,它就不需要休眠。sleep 是一个小众工具,尽管如此,你应该使用某种计时器机制(遗憾的是标准 C++ 没有任何计时器)。 - R. Martinho Fernandes
请注意,负数的 sleep_for 正在测量 sleep_for 调用的开销: 不能期望它花费零时间,因为它必须检查是否应该花费零时间,这需要超过零的时间! 但是请记住,如果您睡眠的时间为负值,则符合睡眠 7 年零 7 个月。 总是符合比请求的时间更长的睡眠时间。 如果您需要荒谬地小的睡眠时间,则需要进行繁忙循环,因为节省 CPU 的睡眠时间最终会等待中断,而其他代码运行... - Yakk - Adam Nevraumont
在Linux内核中,调用nanosleep()的用户空间进程将在下一个调度程序唤醒间隔(通常是“jiffy”)触发,除非该进程被标记为实时优先级。请参见http://elixir.free-electrons.com/linux/latest/source/kernel/time/hrtimer.c#L1513。 - moof2k
3个回答

16
这些数字的原因是什么?
很明显有一个模式,所有的结果都比你请求睡眠的时间多54000纳秒。如果你查看GCC在GNU/Linux上实现的this_thread::sleep_for()如何使用 nanospleep,就会发现它只是使用了nanospleep,正如Cubbi的评论所说,调用该函数可能需要大约50000ns。我猜其中一部分成本是系统调用,因此从用户空间切换到内核空间并返回。
为什么负数睡眠时间会返回200+ ns,而0+纳秒的睡眠时间会导致50,000+纳秒的结果?
我猜测是C库检查了负数,并没有进行系统调用。
作为睡眠时间的负数是否是一个记录/支持的特性,还是我偶然遇到了一些奇怪的错误,不能依靠?
标准没有禁止传递负参数,因此是允许的,函数应该立即返回,因为相对超时指定的时间已经过去。但是你不能依赖于负参数比非负参数返回得更快,这是你特定实现的副产品。
是否有更好的C ++睡眠调用可以给我更一致/可预测的睡眠时间?
我不认为有 - 如果我知道有一个,我们将在GCC中使用它来实现this_thread::sleep_for()
编辑:在GCC的libstdc++的更新版本中,我添加了:
if (__rtime <= __rtime.zero())
  return;

如果请求的持续时间为零或负数,则不会发生系统调用。


libstdc++使用nanosleep实现this_thread::sleep_for,如果tv_sec为负,则nanosleep会立即返回EINVAL。 - Bulletmagnet

6

受到Straight Fast的答案启发,我评估了timer_slack_nsSCHED_FIFO的影响。对于timer_slack_ns,您需要添加

#include <sys/prctl.h> // prctl
⋮
prctl (PR_SET_TIMERSLACK, 10000U, 0, 0, 0);

这意味着对于当前进程,定时器松弛度应设置为10µs,而不是默认值50µs。效果是更好的响应能力,但略微增加了能耗。非特权用户仍然可以运行该进程。要将调度策略更改为SCHED_FIDO,您必须成为“root”。所需的代码为:

#include <unistd.h>    // getpid
#include <sched.h>     // sched_setschedulerconst pid_t pid {getpid ()};
    struct sched_param sp = {.sched_priority = 90};
    if (sched_setscheduler (pid, SCHED_FIFO, &sp) == -1) {
        perror ("sched_setscheduler");
        return 1;
    }

我在带有GUI的桌面系统上运行了Stéphane的代码片段(Debian 9.11,内核4.9.189-3+deb9u2,g++ 9.2 -O3,Intel® Core™ i5-3470T CPU @ 2.90GHz)。第一种情况(后续时间测量)的结果为:

因为没有系统调用存在,延迟约为260ns,并且不受进程设置的影响。对于正态分布的计时,图形是直线,纵坐标为0.5的横坐标值是平均值,而斜率表示标准偏差。测量值与之不同,因为存在较高延迟的异常值。

相比之下,第二种情况(睡眠一纳秒)在处理器设置方面存在差异,因为它包含系统调用。由于睡眠时间非常短,睡眠不会增加任何时间。因此,图形只显示开销

Stéphane所述,开销默认为约64微秒(这里稍微大一些)。通过将timer_slack_ns降低到10微秒,可以将时间缩短到约22微秒。通过调用特权sched_setscheduler(),开销可降至约12微秒。但如图所示,即使在这种情况下,延迟也可能超过50微秒(在0.0001%的运行中)。

测量结果显示了开销与进程设置之间的基本依赖关系。其他测量结果表明,在非GUI XEON服务器系统上,波动幅度比一个数量级低。


3

在内核init/init_task.c中,struct task_struct init_task定义了参数。

.timer_slack_ns = 50000, /* 50 usec default slack */

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