【问题标题】:Why does sleeping increase the execution time of an independent piece of code that is executed before/afterwards?为什么睡眠会增加之前/之后执行的独立代码的执行时间?
【发布时间】:2019-07-07 12:47:56
【问题描述】:

我注意到一些我以前从未见过的非常奇怪的东西。此伪代码中描述了基本设置:

TARGET_LOOP_TIME = X

loop forever:
    before = now()
    payload()
    payload_time = now() - before
    sleep(TARGET_LOOP_TIME - payload_time)

这种设置相当普遍,例如将循环保持在 60 FPS。有趣的部分是:payload_time 取决于睡眠持续时间! 如果TARGET_LOOP_TIME 很高,程序将因此睡眠很多,payload_time 与程序相比要高得多根本不睡觉。

为了衡量这一点,我编写了这个程序:

use std::time::{Duration, Instant};

const ITERS: usize = 100;

fn main() {
    // A dummy variable to prevent the compiler from removing the dummy prime
    // code.
    let mut x = 0;

    // Iterate over different target loop times
    for loop_time in (1..30).map(|n| Duration::from_millis(n)) {
        let mut payload_duration = Duration::from_millis(0);

        for _ in 0..ITERS {
            let before = Instant::now();
            x += count_primes(3_500);
            let elapsed = before.elapsed();
            payload_duration += elapsed;

            // Sleep the remaining time
            if loop_time > elapsed {
                std::thread::sleep(loop_time - elapsed);
            }
        }

        let avg_duration = payload_duration / ITERS as u32;
        println!("loop_time {:.2?}  \t=> {:.2?}", loop_time, avg_duration);
    }

    println!("{}", x);
}

/// Dummy function.
fn count_primes(up_to: u64) -> u64 {
    (2..up_to)
        .filter(|n| (2..n / 2).all(|d| n % d != 0))
        .count() as u64
}

我迭代不同的目标循环时间来测试(1 毫秒到 30 毫秒)并多次迭代 ITERS。我用cargo run --release 编译了这个。在我的机器(Ubuntu)上,程序输出:

loop_time 1.00ms    => 3.37ms
loop_time 2.00ms    => 3.38ms
loop_time 3.00ms    => 3.17ms
loop_time 4.00ms    => 3.25ms
loop_time 5.00ms    => 3.38ms
loop_time 6.00ms    => 4.05ms
loop_time 7.00ms    => 4.09ms
loop_time 8.00ms    => 4.48ms
loop_time 9.00ms    => 4.43ms
loop_time 10.00ms   => 4.22ms
loop_time 11.00ms   => 4.59ms
loop_time 12.00ms   => 5.53ms
loop_time 13.00ms   => 5.82ms
loop_time 14.00ms   => 6.18ms
loop_time 15.00ms   => 6.32ms
loop_time 16.00ms   => 6.96ms
loop_time 17.00ms   => 8.00ms
loop_time 18.00ms   => 7.97ms
loop_time 19.00ms   => 8.28ms
loop_time 20.00ms   => 8.75ms
loop_time 21.00ms   => 9.70ms
loop_time 22.00ms   => 9.57ms
loop_time 23.00ms   => 10.48ms
loop_time 24.00ms   => 10.29ms
loop_time 25.00ms   => 10.31ms
loop_time 26.00ms   => 10.82ms
loop_time 27.00ms   => 10.84ms
loop_time 28.00ms   => 10.82ms
loop_time 29.00ms   => 10.91ms

我绘制了这些数字的图(sleep_timemax(0, loop_time - avg_duration)):

当程序完全不休眠时,有效载荷大约需要 3.3 毫秒(如前三个测量结果所示)。一旦循环在有效载荷之后开始休眠,有效载荷持续时间就会增加!事实上,它会增加到大约 10.5 毫秒。睡眠时间更长并不会增加有效载荷时间。

为什么?为什么这段代码的执行时间取决于我之后(或之前)所做的事情?这对我来说没有意义!看起来 CPU 说“无论如何我都要睡觉了,所以让我们慢慢来”。我考虑了缓存效果,尤其是指令缓存,但是从主存加载指令数据不需要 7ms!这里发生了其他事情!

有没有办法解决这个问题? IE。让有效载荷尽可能快地执行而不考虑睡眠时间?

【问题讨论】:

  • 编译器不能优化count_primes吗?它似乎是一个纯函数,每次迭代都使用相同的参数调用。还应该可以在编译时计算一次 x。
  • @Jens 说得好。它似乎没有优化,但是,是的,我应该修复它。
  • 请注意:这似乎是由于 CPU 频率缩放造成的,并且并非在所有机器上都会发生。然而,这仍然不是一个完整的答案,我希望有人可以提供更多信息。
  • 嗯? payload_duration += elapsed;

标签: performance rust


【解决方案1】:

我很确定这是由 CPU 节流引起的。当 OS 调度程序检测到几乎没有工作要做时,CPU 频率会降低以节省电力。

当你做很多sleeps 的时候,你是在告诉调度器你不那么着急,CPU 可以放轻松。

您可以通过在另一个窗口中以低优先级运行 CPU 密集型任务来看到这种情况。例如,在 Linux 中您可以运行:

$ nice bash -c 'while true ; do : ; done'

同时,在另一个窗口中运行你的程序:

$ cargo run --release
loop_time 1.00ms    => 3.13ms
loop_time 2.00ms    => 3.17ms
loop_time 3.00ms    => 3.19ms
loop_time 4.00ms    => 3.13ms
loop_time 5.00ms    => 3.16ms
loop_time 6.00ms    => 3.22ms
loop_time 7.00ms    => 3.14ms
loop_time 8.00ms    => 3.15ms
loop_time 9.00ms    => 3.13ms
loop_time 10.00ms   => 3.18ms
loop_time 11.00ms   => 3.14ms
loop_time 12.00ms   => 3.17ms
loop_time 13.00ms   => 3.15ms
...

避免这种情况取决于您的操作系统。例如,在 Linux 中,您可以使用 sys/devices/system/cpu/* 选项。我认为UPower 提供了一些功能来从非根应用程序管理它。如果有一个 crate 来管理这个跨系统就好了,但我不知道。

如果您不介意浪费的功率,那么解决此问题的一种简单但巧妙的方法就是运行一个繁忙循环的空闲线程。

std::thread::spawn(|| {
    use thread_priority::*; //external crate thread-priority
    let thread_id = thread_native_id();
    set_thread_priority(
        thread_id,
        ThreadPriority::Min,
        ThreadSchedulePolicy::Normal(NormalThreadSchedulePolicy::Idle),
    )
    .unwrap();
    loop {}
});

当然,如果你只是想避免这段代码的节流,你可以做一个忙碌的等待:

    //if loop_time > elapsed {
    //    std::thread::sleep(loop_time - elapsed);
    //}
    // Busy-wait the remaining time, to avoid CPU throttling
    while loop_time > before.elapsed() {
        //you may want to try both with and without yield
        std::thread::yield_now();
    }

【讨论】:

  • 谢谢! CPU频率的事情是有道理的。关于解决方案:我可能只是忙于等待而不是在我的主线程中睡觉,而不是忙碌的线程。
  • 我还有一个额外的问题(如果有人能回答这个问题):根据this paper(如果我理解正确的话)两个 CPU 频率之间的延迟相当低(
  • @LukasKalbertodt:事实上,在您的图表中,您可以看到操作系统实际上正在尝试预测您将要入睡的时间,并降低了让您仍然入睡的时间. CPU 频率当然有上限。
  • @LukasKalbertodt 这些设置是特定于操作系统的。在 Linux 上,您可以echo performance > /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor,在 Windows 上您可以选择省电设置。还有 Turbo Boost 动态增加最大值。如果有热预算,内核的频率。我建议在 BIOS 中禁用功率调节和涡轮增压以进行基准测试。
  • @LukasKalbertodt:我还要注意,除了节流之外,还有 其他 原因导致核心频率发生变化。在多核 CPU 上,也存在缩放:即,如果使用单个内核,则它可以达到比所有内核都忙时更高的频率,这既是出于电源供应的原因,也是出于散热的原因。此外,使用特别耗电的指令(例如 AVX512)也可能触发这种频率切换。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-11-18
  • 1970-01-01
  • 1970-01-01
  • 2019-10-03
相关资源
最近更新 更多