为什么休眠会增加在之前/之后执行的独立代码的执行时间?

时间:2019-07-07 12:47:56

标签: performance rust

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

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)):

enter image description here

当程序完全不休眠时,有效负载大约需要3.3ms(如前三个测量结果所示)。负载之后,一旦循环开始休眠,则负载持续时间就会增加!实际上,它停留在大约10.5ms处。睡得更久不会增加有效负载时间。

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

是否可以解决此问题?即使有效负载尽快执行而与睡眠时间无关?

1 个答案:

答案 0 :(得分:9)

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

当您执行许多sleep时,您是在告诉调度程序您并不着急,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提供了一些功能来从非根应用程序对其进行管理。如果有一个箱子可以管理这个跨系统,那就太好了,但我什么都不知道。

如果您不介意浪费的功率,一种简单但棘手的解决方法就是运行带有繁忙循环的空闲线程。

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();
    }