Почему спящий режим увеличивает время выполнения независимого фрагмента кода, который выполняется до / после? - PullRequest
17 голосов
/ 07 июля 2019

Я заметил нечто действительно странное, чего раньше не видел.Базовая настройка описана в этом псевдокоде:

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_time равен max(0, loop_time - avg_duration)):

enter image description here

Когда программа вообще не спит, полезная нагрузка требует приблизительно 3,3 мс (как показывают первые три измерения).Как только цикл начинает спать после полезной нагрузки, продолжительность полезной нагрузки увеличивается!На самом деле, он увеличивается примерно до 10,5 мс, где он остается.Сон еще дольше не увеличивает время полезной нагрузки.

Почему?Почему время выполнения фрагмента кода зависит от того, что я делаю потом (или раньше)? Это не имеет смысла для меня!Похоже, процессор говорит: «В любом случае, я буду спать потом, так что давай потихоньку».Я думал об эффектах кэширования, особенно о кеше инструкций, но загрузка данных инструкций из основной памяти не занимает 7 мс!Здесь что-то еще происходит!

Есть ли способ это исправить?Т.е. чтобы полезная нагрузка выполнялась как можно быстрее независимо от времени ожидания?

1 Ответ

9 голосов
/ 07 июля 2019

Я совершенно уверен, что это вызвано дросселированием процессора. Когда планировщик ОС обнаруживает, что работы мало, частота ЦП снижается для экономии энергии.

Когда вы делаете много sleep с, вы говорите планировщику, что вы не спешите, и процессор может с этим справиться.

Вы можете увидеть, что это так, запустив задачу с интенсивным использованием ЦП в другом окне с низким приоритетом. Например, в 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 предоставляет некоторые функции для управления им из приложения без полномочий root. Было бы неплохо, если бы существовал ящик, управляющий этой кросс-системой, но я не знаю ни одного.

Простой, но хакерский способ исправить это, если вы не возражаете против потраченной впустую мощности, это просто запустить свободный поток с занятым циклом.

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();
    }
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...