Accurate Timing

ByJason Bagley

Published Sun Aug 01 2021

In many tasks we need to do something at given intervals of time. The most obvious ways may not give you the best results.

Time? Meh.

The most basic tasks that don't have what you might call CPU-scale time requirements can be handled with the usual language and framework features. These can be things like timers, or simply a call to a sleep(number_of_milliseconds) function.

void RunTimedThing(){   while (true)   {      DoMyThing();      sleep(1);      // wake up about 1 millisecond later to do it again.   }}

Exercise 1: How much time is there between each call to DoMyThing()?

Most OS thread scheduling is not guaranteed, so your thread isn't usually executed again exactly when you want it to be. If you are fine with your thing being done again not exactly 1 ms later, this approach is acceptable. But if you need accuracy to the millisecond, or you need it to run more frequently than 1 ms, it is time to level up.

When it positively, absolutely has to be done on time

Now you will need a high resolution timer. These have millisecond or even nanosecond accuracy. It used to be more of a _maybe-you-can-maybe-you-can't kind of feature feature, but with modern hardware, even the small guys, you usually will be able to get all the way up to nanosecond precision.

For C++, std::chrono provides such facilities.

using namespace std::chrono_literals;              // For using 1ms as a const literal for 1 millisecondusing namespace std::chrono;                       // For the sake of brevity and sanityusing Clock = std::chrono::high_resolution_clock;  // For ease in changing our timing resolutionvoid RunTimedThingAccurately(){   const auto kMyThingFrequency = 1ms;   const auto kMaxSleepTime = kMyThingFrequency / 2.0; // set to something less than the frequency   auto nextTime = Clock::now();   while (true)   {      const auto currentTime = Clock::now();      const auto remainingTime = duration_cast<microseconds>(nextTime - currentTime);      if (remainingTime.count() <= 0LL)      {         DoMyThing();         nextTime = currentTime + kMyThingFrequency + remainingTime;         sleepTime = maxSleepTime;      }      else      {         sleepTime = std::min(maxSleepTime, remainingTime);      }        std::this_thread::sleep_for(remainingTime);   }}

Exercise 2: Now how much time is there between each call to DoMyThing()?

Notice that it still isn't enough to just sleep for the interval at which you want to do your thing. Even the higher resolution will have some slop, called jitter, around its timings. To combat that we do a little polling, waking up to see if it is time then sleeping for the remainder of the time until we do our thing. You can try to optimize kMaxSleepTime to improve accuracy and minimize times being awakened before the work needs to be done.

You may have noticed this subtlety, it is where some magic happens

nextTime = currentTime + kMyThingFrequency + remainingTime;

The remaining time will be negative or zero when this block executes, so it automatically handles being woken up a little late. Otherwise, nextTime would accumulate those errors.

Another subtlety about kMaxSleepTime. DoMyThing() obviously must take less time than kMyThingFrequency. How long it takes will influence the value you assign to kMaxSleepTime which needs to be less than the difference between how long your thing takes and how often your thing needs to happen.

A note on std::chrono

The std::chrono code can be a little ugly without the aliases. But the unit handling is superb. Once you are used to it you may wish to do similar things for other values with units. It is well worth getting comfortable with it when you have to do any time based calculations.

Previous

Spot the Vulnerability: Data Ranges and Untrusted Input
In 1997, a flaw was discovered in how Linux and Windows handled IP fragmentation, a Denial-of-Service vulnerability which allowed systems to be crashed remotely.