Show Lecture.Chrono as a slide show.
CS253 Chrono
Roman Timekeeping
Inclusion
To use the chrono features, you need to:
#include <chrono>
using namespace std::chrono;
This introduces a lot of new symbols, quarantined to their own
namespace std::chrono
to avoid conflicts.
Time in Real Life
- Our timekeeping notation is one of the worst systems ever devised.
- If you proposed such a system, you’d get laughed out of the room.
- Consider numbers: we use base 10 notation. Each digit is worth
ten times more than the digit to its right. Base 10 is a bit weird,
but we have that many fingers. ✋️🤚
It works ok.
- Imagine what a disaster it would be if each digit place used a
different multiplier than 10. Some could be zero-based, some might be
one-based, some could be words, and some places might have multiple
definitions.
- Real-life timekeeping is just that bad.
Time in Real Life
The common timekeeping system in the United States:
- Base unit: seconds (00…59) except for leap seconds
- Next unit: minutes (00…59)
- Next unit: hours (00…23 or 12AM, 1AM…11AM,
12PM, 1PM…11PM) except for DST change days
- Next unit: days (Mon…Sun or Sun…Sat, no generally accepted numbers)
- Next unit: weeks (0…53 or 1…54)
-or-
Next unit: months (Jan…Dec or 1…12)
- Next unit: years (…, 3BC, 2BC, 1BC,
1AD, 2AD, 3AD, …, 2024AD, …)
or BCE/CE
Time in C++
Given that monstrosity, time software is going to be complicated,
because timekeeping in real life is complicated. There’s no way around
that.
Time in C++
- duration: a length of time
- time_point: an instant in time
- August 30, 2024, at 06:20 PM
- the launch of Sputnik
- 2021‒01‒20T12:00:00-0500
- clock: an epoch + units
- A duration is not much more than a number and units:
3ms, 1.25 hours, 100 years
- These are stored in the tempated class duration:
template<typename Representation, class Period>
Representation
is int, long, double, etc.
Period
is a number of seconds, expressed as a ratio type:
duration::count()
returns the count.
- The
Period
is a compile-time construct.
- E.g., if the compiler knows, at compile-time, that it’s
converting days to hours, it efficiently multiplies by 24 using
shifts & adds, instead of slow multiplication.
Predefined duration types
The following types are predefined. They might not actually be
long, but they’re an integer type that is big enough.
using hours = duration<long, ratio<60*60,1>>;
using minutes = duration<long, ratio<60,1>>;
using seconds = duration<long, ratio<1,1>>;
using milliseconds = duration<long, ratio<1,1000>>;
using microseconds = duration<long, ratio<1,1'000'000>>;
using nanoseconds = duration<long, ratio<1,1'000'000'000>>;
C++20 adds days, weeks, months, and years.
Perhaps their variable nature (DST, leap years) caused a delay in
their standardization.
- Remember how
42UL
means unsigned long, and 4.5L
means long double?
- Several suffixes exist for duration constants:
- number
h
(hours)
- number
min
(minutes)
(as if C++ doesn’t have enough min
already)
- number
s
(seconds)
(as opposed to "
string object "s
)
- number
ms
(milliseconds)
- number
us
(microseconds)
(alas, us
,
not μs
)
- number
ns
(nanoseconds)
12h
≡ hours(12)
, 123ms
≡ milliseconds(123)
, etc.
if (0.5h - 15min == 900s && seconds(5) == 5000000us)
cout << "Hooray!\n";
Hooray!
Extracting the units
Even though 1h == 60min
, they are represented differently:
auto a = 1h;
auto b = 60min;
if (a == b)
cout << a.count() << ' ' << b.count();
1 60
This works because duration comparison via ==
is smart.
It doesn’t just naïvely compare the .count()
values—it scales
them according to their ratios. Probably some sort of
Least Common Multiple business.
We’re comfortable with 3 == 3.0
, even though they’re represented
differently. This is just more of the same.
// How long is a semester, in seconds?
using weeks = duration<long, ratio<7*24*60*60>>; // until C++20
weeks w(15);
seconds s = w;
cout << w.count() << " weeks is " << s.count() << " seconds.\n";
15 weeks is 9072000 seconds.
w
is, essentially, a long containing 15.
- It also has compile-time information of the ratio of
weeks to seconds (604800:1).
s
is, essentially, a long containing 9072000.
- It also has compile-time information of the ratio of
seconds to seconds (1:1).
Define your own units
“In the future, everyone will be world-famous for 15 minutes.”
—Andy Warhol
using warhol = duration<float, ratio<15*60>>;
warhol teaching(50min);
cout << "Famous for " << teaching.count() << " Warhols.\n";
Famous for 3.33333 Warhols.
Duration conversion
Durations can be assigned, sometimes:
milliseconds ms = 2s;
cout << ms.count();
2000
but not if there would be a loss of precision:
seconds s = 1999ms; // 🦡 1s or 2s?
cout << s.count();
c.cc:1: error: conversion from ‘duration<[...],ratio<[...],1000>>’ to
non-scalar type ‘duration<[...],ratio<[...],1>>’ requested
To force a loss-of-precision assignment, use duration_cast(),
which truncates (discards the fractional part):
seconds s = duration_cast<seconds>(4567ms);
cout << s.count();
4
or use floating-point storage:
using floatsecs = duration<double, ratio<1,1>>;
floatsecs s = 5678ms;
cout << s.count();
5.678
If the storage type of a duration is a floating-point type, the type
system figures that you’ve got it covered, as in the Warhol example.
Dates are tricky!
How many days in a year? 365, of course! Except …
- Add a leap day (February 29) in years divisible by 4.
- However, years divisible by 100 are not leap years
- However, years divisible by 400 are leap years.
So, an average Gregorian Year is
365 + 1⁄4 − 1⁄100 + 1⁄400 = 365.2425 days,
or 365.2425 × 24 × 60 × 60 = 31556952 seconds.
The actual astronomical year is a messy physical thing that refuses
to conform to our puny approximations.
Good enough?
Let’s see how big a quantity of nanoseconds we can represent,
and translate that to years:
constexpr long spy = 365.2425 * 24 * 60 * 60; // seconds/year
auto nsmax = nanoseconds::max();
cout << "Max ns: " << nsmax.count() << '\n';
using dyears = duration<double, ratio<spy>>;
dyears y = nsmax;
cout << "Max ns in years: " << y.count() << '\n';
Max ns: 9223372036854775807
Max ns in years: 292.277
Good enough. If we’re measuring something down to the nanosecond
level, it had better be over fairly quickly.
- A time_point is a point in time, an epoch:
- creation of the world
- birth of some popular dude
- the founding of the city or nation
- beginning of the semester
- January 1, 1970
- and a duration, which we’ve discussed above.
time_point examples:
- Midterm #2 (start of CSU semester + 10 weeks of classes)
- American Revolution (Common Era start + 1776 years)
Clocks
- C++ defines several clocks, which yield time_point values.
- system_clock
- Think “wall clock”, the clock on the wall, time for humans.
- Measure actual time, like 6:20ᴘᴍ,
and historic things like dates & ages.
- Might have crude precision, e.g., seconds.
- Epoch is assumed to be a significant time in the past, e.g., 1970.
- steady_clock
- Think “real-time clock”.
- Measure elapsed time—how long a program runs.
May only be able to deal with a small total amount of time.
- Assumed to have good precision, e.g., nanoseconds.
- Epoch may be recent, e.g., system boot.
Using a clock
- A clock has a
::now()
static method that yields a
time_point representing now.
- A time_point has a
.time_since_epoch()
method that yields a
duration since the epoch, in whatever units the clock naturally
deals with.
constexpr long spy = 365.2425 * 24 * 60 * 60; // seconds/year
using dyears = duration<double, ratio<spy>>;
auto now = system_clock::now(); // a time_point
dyears y = now.time_since_epoch(); // a duration
cout << y.count() << " years since the epoch.\n";
54.6651 years since the epoch.
Using a clock
system_clock has static methods ::to_time_t()
and ::from_time_t()
,
which converts a time_point
to & from seconds since the Linux time_t
epoch (generally the start of 1970).
auto now = system_clock::now(); // time_point
time_t t = system_clock::to_time_t(now); // time_t
cout << t << " seconds since 1970 started.\n";
cout << ctime(&t); // char *
1725063631 seconds since 1970 started.
Fri Aug 30 18:20:31 2024
Age
How old is your instructor? Even that can be represented!
constexpr long spy = 365.2425 * 24 * 60 * 60; // seconds/year
using dyears = duration<double, ratio<spy>>;
using months = duration<long, ratio<spy,12>>; // till C++20
using days = duration<long, ratio<24*60*60>>; // till C++20
auto birth = system_clock::from_time_t(-0x16e06d50); // 🍼👶
auto age = system_clock::now() - birth;
cout << duration_cast<days>(age).count() << " days\n"
<< duration_cast<months>(age).count() << " months\n"
<< dyears(age).count() << " years\n";
24408 days
801 months
66.8274 years
2038‒01‒18T20:14:08
In January 2038, Linux time (seconds since the start
of 1970) will reach 231, or 8000 000016.
This will cause problems with systems that store time_t as a 32-bit
signed integer.
typedef duration<double,ratio<60*60*24>> ddays; // until C++20
auto tp = system_clock::from_time_t(0x7fffffff); // 2038‒01‒18
ddays ndays = tp - system_clock::now();
cout << ndays.count() << " days until the y2038 bug!\n"
<< "My time_t is " << numeric_limits<time_t>::digits << " bits.\n";
4889.12 days until the y2038 bug!
My time_t is 63 bits.
Timing
Use steady_clock to time code:
// Compiled with -O0 so the loop won’t vanish.
auto start = steady_clock::now(); // time_point
for (long i=0; i<1e8; i++); // count to 10⁸
auto elapsed = steady_clock::now() - start;
auto ms = duration_cast<milliseconds>(elapsed); // duration
cout << "That took " << ms.count() << "ms.";
That took 185ms.
steady_clock most likely has much better than millisecond resolution,
so we duration_cast() to milliseconds.
Reminder
Don’t forget using namespace std::chrono;
in your code!