I need to create a loop where I display some information to the screen every 1/30th of a second. I have been using a rather naïve approach where I keep track of the elapsed time during the start and the end of one iteration loop and subtract this value from the duration of the frame. If that time difference is greater than 0, I call std:sleep_for()
passing that time difference.
While this works in practice, when I measure the actual time over a certain number of processed cycles (say 120), I notice a significant time difference between the performance of the system and the expected result. Where I should see more or less 4 seconds, I systematically get times over 5 seconds.
Here is some basic code (that simply calls sleep_for
120 times with a duration if 1/30th of a second):
#include <chrono>
#include <iostream>
using namespace std::chrono;
std::chrono::time_point start = std::chrono::high_resolution_clock::now();
int main()
{
constexpr std::chrono::duration<double, std::micro> one_cycle_duration{1.0 / 30. * 1000000};
auto global_start = std::chrono::high_resolution_clock::now();
for (uint32_t i = 0; i < 120; ++i) {
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration<double, std::milli>(end - start);
//std::cerr << i << " " << duration.count() << std::endl;
auto wait_for = std::chrono::duration<double, std::milli>(one_cycle_duration - duration);
//std::cerr << i << " " << duration.count() << " " << wait_for.count() << std::endl;
std::this_thread::sleep_for(wait_for);
start = std::chrono::high_resolution_clock::now();
}
auto global_end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double, std::milli> global_elapsed = global_end - global_start;
std::cerr << global_elapsed.count() << std::endl;
return 0;
}
Result:
5643.35
I understand that std::sleep_for()
doesn't guarantee that it will exactly wait for the passed duration and is somehow imperfect due to synchronization issues and other things taking place in the loop. Still, I wasn't expecting such a significant time difference. I am already using a high-resolution clock.
So my question is/are:
- do I do something wrong (to start with)?
- if not, how can one get significantly better results (recommended practice)? E.g., if I run the loop 120 times with 1/30th of a second duration, I should get a result fairly close to 4 seconds, not 4.5, not 5.5 seconds as I get now.
Again, I tried to compensate in the loop by only waiting for the expected cycle duration minus the time it took to process the previous iteration. I was hoping that, over time this would self-correct the issue, but I have yet to get significant improvements (as shown in the code above).
Edit/Solution (an improvement, really):
I am editing the question with the hint provided by @HowardHinnant and @NathanOliver (credit go to them -- see in the comments)
- get the time when the app starts
- rather than using
sleep_for
usesleep_until
where the duration passed to the function isglobal_start + number_of_cycles * time_per_cycle
. - ps: i have been using
steady_clock
overhigh_resolution_clock
as mentioned in a post linked in the comment. While the argument for usingsteady_clock
are understandable, it made no difference in practice.
auto global_start = std::chrono::steady_clock::now();
for (uint32_t i = 0; i < 120; ++i) {
std::this_thread::sleep_until(global_start + i * one_cycle_duration);
}
auto global_end = std::chrono::steady_clock::now();
std::chrono::duration<double, std::milli> global_elapsed = global_end - global_start;
std::cerr << global_elapsed.count() << std::endl;
Result:
3978.73
This is also a basic problem in numerical precision that I should have thought of: you don't want to accumulate an "error" over time. Instead, you should be aiming for the right solution at each iteration. Thanks again to those who pointed me in the right direction.