16

如果我并排放置两个调用以确定最小的可测量持续时间:

// 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+ 纳秒?
  • 作为睡眠时间的负数是记录/支持的功能,还是我不小心偶然发现了一些我无法依赖的奇怪错误?
  • 有没有更好的 C++ 睡眠调用可以给我更一致/可预测的睡眠时间?
4

3 回答 3

15

什么可以解释这些数字?

有一个非常明显的模式,您的所有结果始终比您请求睡眠的时间长 54000ns。如果您查看 GCCthis_thread::sleep_for()是如何在 GNU/Linux 上实现的,您会发现它只是使用nanospleep,并且正如 Cubbi 的评论所说,调用该函数可能需要大约 50000ns。我猜其中一些成本是进行系统调用,因此从用户空间切换到内核并返回。

为什么睡眠时间为负数会返回 200+ ns,而睡眠时间为 0+ 纳秒会导致 50,000+ 纳秒?

我猜我会说 C 库检查负数并且不进行系统调用。

作为睡眠时间的负数是记录/支持的功能,还是我不小心偶然发现了一些我无法依赖的奇怪错误?

标准不禁止传递负参数,因此是允许的,并且函数应该“立即”返回,因为相对超时指定的时间已经过去。但是,您不能依赖负参数比非负参数返回得更快,这是您的特定实现的产物。

有没有更好的 C++ 睡眠调用可以给我更一致/可预测的睡眠时间?

我不这么认为——如果我知道一个,那么我们会在 GCC 中使用它来实现this_thread::sleep_for().

编辑:在 GCC 的 libstdc++ 的更新版本中,我添加了:

if (__rtime <= __rtime.zero())
  return;

因此,当请求零或负持续时间时,不会有系统调用。

于 2013-08-06T16:59:47.010 回答
5

受到Straight Fast答案的启发,我评估了timer_slack_ns和 的效果SCHED_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_setscheduler
⋮
    const 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µs(这里有点大。)。timer_slack_ns通过降低到 10µs ,时间可以减少到大约 22µs 。通过调用特权 sched_setscheduler(),开销可以减少到大约 12µs。但如图所示,即使在这种情况下,延迟也可能超过 50µs(在 0.0001% 的运行中)。

测量结果显示了流程设置开销的基本相关性。其他测量表明,非 GUI XEON 服务器系统的波动要低一个数量级以上。

于 2020-02-10T15:20:39.373 回答
3

在内核 init/init_task.c 中 struct task_struct init_task 定义的参数

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

它添加到 hrtimer_nanosleep() 内核函数中的非 RT 进程,以减少计时器的 hardirqs。

于 2019-04-16T04:32:47.287 回答