I built a local competitive programming judge a while back where I fell into the exact trap of manually correlating steady_clock and system_clock.
I was timestamping everything using steady_clock to strictly enforce the time limits, and then just applied a calculated offset to convert that to wall time for the "Submission Date" display.
It worked in testing, but during a long stress-test session, my laptop did an NTP sync. The logs then showed submissions appearing to happen before the source code file was last modified, which confused the caching logic. I was essentially betting that system_clock was stable relative to steady_clock over the process lifetime.
I eventually refactored to exactly what the article suggests: use steady_clock strictly for the runtime enforcement (is duration < 2.0s?), but capture system_clock::now() explicitly at the submission boundary for the logs, rather than trying to do math on the steady timestamp.
Also, +1 for std::chrono::round in C++20. I’ve seen code where duration_cast was used to "round" execution times to milliseconds for display, but it was silently flooring them. In a competitive programming context, reporting 1000ms when the actual time was 1000.9ms is a misleading difference because the latter gets Time Limit Exceeded error. Explicit rounding makes the intent much clearer.
The thing I ran into most recently is that std::chrono (weirdly?) only supports clocks with compile-time fixed fractional conversion to reference time (~seconds). E.g., you can't implement a std::chrono clock where the count() unit is the native CPU's cycle counter, which will have some runtime-determined conversion to seconds. The types make it impossible.
std::chrono counts the number of ticks of a given periodicity. The periodicity does have to be a known compile-time ratio expressible as a fraction of seconds, but you can use a floating-type type to count the number of ticks.
std::chrono is meant to take advantage of information about periodicity at compile time, but if you want to count in terms of a dynamic periodicity not known until runtime, you can still do things. E.g. use time_t and some wrapper functions, or just pick one (or several) std::chrono base durations such as standardizing on nanoseconds, and then just count floating-point ticks.
> The periodicity does have to be a known compile-time ratio expressible as a fraction of seconds
Right, that's the problem I'm describing.
> or just pick one (or several) std::chrono base durations such as standardizing on nanoseconds, and then just count floating-point ticks.
None of this really fits well -- the idea is to count in integer ticks (invariant cycles), without doing expensive division or floating point math. It's like steady clock, but in ticks instead of nanos.
C++ chrono is weird. It's both over-abstracted and yet has some thoughtless features that just negate this over-abstraction.
I remember not being able to represent the time in fractional units, like tertias (1/60-th of a second) but that was mostly an academic problem. More importantly, it was not possible to express duration as a compound object. I wanted to be able to represent the dates with nanosecond precision, but with more than 250 years of range. I think it is still not possible?
Fractional time shouldn't be an issue as long as the ratio is a rational number. It's how durations are already handled behind the scenes.
Likewise it's annoying to cook up your own type, but as long as you can come up with 'number of ticks' type that acts like an arithmetic type then it should work with std::chrono. E.g. if I just Boost's Multiprecision library as a quick example:
#include <chrono>
#include <iostream>
#include <boost/multiprecision/cpp_int.hpp>
using namespace std::chrono_literals;
int main()
{
using BigInt = boost::multiprecision::uint128_t;
using BigNanos = std::chrono::duration<BigInt, std::nano>;
std::cout << "Count for 1s: " << BigNanos(1s).count() << "\n";
std::cout << "Count for 300 years: " << BigNanos(std::chrono::years(300)).count() << "\n";
}
Then that code (when compiled with -std=c++20 or better) should output:
Count for 10s: 1000000000
Count for 300 years: 9467085600000000000
If you're willing to use gcc/clang builtins for 128-bit types you won't even need Boost or your own custom type to do basic arithmetic (but serializing the result may be difficult, not sure how gcc/clang handle that for custom 128-bit types).
I was timestamping everything using steady_clock to strictly enforce the time limits, and then just applied a calculated offset to convert that to wall time for the "Submission Date" display. It worked in testing, but during a long stress-test session, my laptop did an NTP sync. The logs then showed submissions appearing to happen before the source code file was last modified, which confused the caching logic. I was essentially betting that system_clock was stable relative to steady_clock over the process lifetime.
I eventually refactored to exactly what the article suggests: use steady_clock strictly for the runtime enforcement (is duration < 2.0s?), but capture system_clock::now() explicitly at the submission boundary for the logs, rather than trying to do math on the steady timestamp.
Also, +1 for std::chrono::round in C++20. I’ve seen code where duration_cast was used to "round" execution times to milliseconds for display, but it was silently flooring them. In a competitive programming context, reporting 1000ms when the actual time was 1000.9ms is a misleading difference because the latter gets Time Limit Exceeded error. Explicit rounding makes the intent much clearer.
reply