我注意到以前从未见过的非常奇怪的事情。此伪代码描述了基本设置:
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)
):
当程序完全不休眠时,有效负载大约需要3.3ms(如前三个测量结果所示)。负载之后,一旦循环开始休眠,则负载持续时间就会增加!实际上,它停留在大约10.5ms处。睡得更久不会增加有效负载时间。
为什么?为什么这段代码的执行时间取决于我之后(或之前)要做的事情?对我来说这没有意义! CPU似乎说“无论如何我以后都会睡觉,所以让我们慢慢来”。我考虑过缓存效果,尤其是指令缓存的效果,但是从主存储器加载指令数据不需要7毫秒!这里还有其他事情!
是否可以解决此问题?即使有效负载尽快执行而与睡眠时间无关?
答案 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();
}