Reputation: 744
In my current project I have a C struct to hold a timestamp, which looks like this:
struct timestamp {
uint16_t year;
uint8_t month;
uint8_t day;
uint8_t hour;
uint8_t min;
uint8_t second;
}
Now I want to calculate the difference between two of these timestamps in seconds. For now I'm converting my timestamp struct to the C standard struct struct tm
(defined in <time.h>
). Then I convert the struct to time_t
with mtkime()
, which takes a pointer to a struct tm
and returns a time_t
. And difftime()
to calculate the difference between two time_t
's in seconds.
I don't want to write my own difftime()
, since I do not want to deal with leapyears or even worse leap seconds by myself, and I don't use the struct tm
in my code because it holds a lot of values I don't frequently need (like week-day or year-day).
Here is an example, what I do at the moment:
void customTimestampToStructTM(struct customTimestamp *in, struct tm *out) {
out->tm_year = in->year;
out->tm_mon = in->mon;
out->tm_mday = in->day;
out->tm_hour = in->hour;
out->tm_min = in->min;
out->tm_sec = in->sec;
}
void someFunction() {
struct customTimestamp c1;
struct customTimestamp c2;
// Fill c1 and c2 with data here.
struct tm tmpC1;
struct tm tmpC2;
customTimestampToStructTM(&c1, &tmpC1);
customTimestampToStructTM(&c2, &tmpC2);
double diffInSeconds = difftime(mktime(tmpC1), mktime(tmpC2));
// Use diffInSeconds
}
This works, but seem incredibly inefficient. How can I speed this up? I read here that mktime
doesn't use the other fields in the struct tm
- except for isdst
. Is there a convenient way to convert my struct to time_t
, without using struct tm
as a bridge and without the need to deal with leapyears/seconds?
To clarify: time_t
holds dates in amount of milliseconds passed since a specific date (1 Jan 1970).
Upvotes: 1
Views: 2471
Reputation: 39426
I shall assume the timestamps always refer to UTC time, in which case Daylight Savings Time does not apply (and you'll want to specify tm.isdst = 0
).
(I suspect that it would be optimal in this case to have the time_t
in UTC, but the broken-down fields in local time. Below, I'll just assume that the local timezone is UTC, with no DST changes.)
Personally, I'd save both the time_t
and the split fields, and use helper functions to set/modify the timestamps.
#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <stdint.h>
#include <stdio.h>
#include <time.h>
#include <errno.h>
struct timestamp {
time_t packed;
int16_t year;
int8_t month;
int8_t day;
int8_t hour;
int8_t min;
int8_t sec;
};
static inline int set_timestamp_time(struct timestamp *const ref, const time_t when)
{
struct tm temp = { 0 };
if (!gmtime_r(&when, &temp))
return ERANGE; /* Year didn't fit. */
ref->packed = when;
ref->year = temp.tm_year + 1900;
ref->month = temp.tm_mon + 1;
ref->day = temp.tm_mday;
ref->hour = temp.tm_hour;
ref->min = temp.tm_min;
ref->sec = temp.tm_sec;
return 0;
}
static inline int set_timestamp(struct timestamp *const ref,
const int year, const int month, const int day,
const int hour, const int min, const int sec)
{
struct tm temp = { 0 };
temp.tm_year = year - 1900;
temp.tm_mon = month - 1;
temp.tm_mday = day;
temp.tm_hour = hour;
temp.tm_min = min;
temp.tm_sec = sec;
/* We assume timestamps are in UTC, and Daylight Savings Time does not apply. */
temp.tm_isdst = 0;
ref->packed = mktime(&temp);
ref->year = temp.tm_year + 1900;
ref->month = temp.tm_mon + 1;
ref->day = temp.tm_mday;
ref->hour = temp.tm_hour;
ref->min = temp.tm_min;
ref->sec = temp.tm_sec;
return 0;
}
set_timestamp()
sets the timestamp based on split fields (year, month, day, hour, minute, second), whereas set_timestamp_time()
sets it based on POSIX time. Both functions always update all timestamp fields.
This allows fast access to both the Unix time, and the split fields, but uses slightly more memory (8 bytes per timestamp, typically; 160 megabytes additional memory for 20 million records).
If you do not need the exact number of seconds between two timestamps, but only use the time_t
to compare whether one is before or after another, then I recommend using a single int64_t
to describe your timestamps:
#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <stdint.h>
#include <time.h>
/* Timestamps can be compared as integers, like POSIX time_t's.
* The difference between two timestamps is at least their
* difference in seconds, but may be much larger.
*
* Zero is not a valid timestamp!
*/
typedef int64_t timestamp;
#define TIMESTAMP_YEAR(t) ((int64_t)(t) / 67108864)
#define TIMESTAMP_MONTH(t) (((uint32_t)(t) >> 22) & 15)
#define TIMESTAMP_DAY(t) (((uint32_t)(t) >> 17) & 31)
#define TIMESTAMP_HOUR(t) (((uint32_t)(t) >> 12) & 31)
#define TIMESTAMP_MIN(t) (((uint32_t)(t) >> 6) & 63)
#define TIMESTAMP_SEC(t) ((uint32_t)(t) & 63)
static inline time_t timestamp_time(const timestamp t, struct tm *const tm_to)
{
struct tm temp = { 0 };
time_t result;
uint32_t u = t & 67108863U;
temp.tm_sec = u & 63; u >>= 6;
temp.tm_min = u & 63; u >>= 6;
temp.tm_hour = u & 31; u >>= 5;
temp.tm_mday = u & 31; u >>= 5;
temp.tm_mon = u - 1;
temp.tm_year = ((int64_t)t / 67108864) - 1900;
/* UTC time, thus Daylight Savings Time does not apply. */
temp.tm_isdst = 0;
result = mktime(&temp);
if (tm_to)
*tm_to = temp;
return result;
}
static inline double difftimestamp(const timestamp t1, const timestamp t2)
{
return difftime(timestamp_time(t1, NULL), timestamp_time(t2, NULL));
}
static inline timestamp set_timestamp_time(const time_t when, struct tm *const tm_to)
{
struct tm temp = { 0 };
if (!gmtime_r(&when, &temp))
return 0;
if (tm_to)
*tm_to = temp;
return (int64_t)67108864 * ((int64_t)temp.tm_year + 1900)
+ (int64_t)((temp.tm_mon + 1) << 22)
+ (int64_t)(temp.tm_mday << 17)
+ (int64_t)(temp.tm_hour << 12)
+ (int64_t)(temp.tm_min << 6)
+ (int64_t)temp.tm_sec;
}
static inline timestamp set_timestamp(const int year, const int month, const int day,
const int hour, const int min, const int sec,
struct tm *const tm_to, time_t *const time_to)
{
struct tm temp = { 0 };
temp.tm_year = year - 1900;
temp.tm_mon = month - 1;
temp.tm_mday = day;
temp.tm_hour = hour;
temp.tm_min = min;
temp.tm_sec = sec;
temp.tm_isdst = 0; /* Since timestamps are in UTC, Daylight Savings Time does not apply. */
if (time_to)
*time_to = mktime(&temp);
if (tm_to)
*tm_to = temp;
return (int64_t)67108864 * ((int64_t)temp.tm_year + 1900)
+ (int64_t)((temp.tm_mon + 1) << 22)
+ (int64_t)(temp.tm_mday << 17)
+ (int64_t)(temp.tm_hour << 12)
+ (int64_t)(temp.tm_min << 6)
+ (int64_t)temp.tm_sec;
}
The idea here is that you can compare two timestamp
s trivially; a < b
if and only if timestamp a
is before b
; a == b
if and only if the timestamps refer to the same second, and a > b
if and only if a
is after b
. At the same time, the accessor macros TIMESTAMP_YEAR(a)
, TIMESTAMP_MONTH(a)
, TIMESTAMP_DAY(a)
, TIMESTAMP_HOUR(a)
, TIMESTAMP_MIN(a)
, and TIMESTAMP_SEC(a)
allow very fast access to the individual date and time components. (On typical Intel/AMD 64-bit hardware, it may be even faster than accessing byte-sized fields.)
The difftimestamp()
function yields the exact number of seconds between two timestamps, but it is quite slow. (As I mentioned, this approach is best only if you don't need this, or only need it rarely.)
timestamp_time()
converts a timestamp to a time_t
, optionally saving the struct tm
fields to a specified pointer (if not NULL
).
set_timestamp_time()
returns a timestamp based on a time_t
. If the year does not fit in an int, it will return 0 (which is NOT a valid timestamp). If the second parameter is not NULL
, the corresponding struct tm
is stored there.
set_timestamp()
returns a timestamp based on year, month, day, hour, minute, and second. If they refer to an impossible date or time, they are corrected (by mktime()
). If the seventh parameter is not NULL, the resulting struct tm
is stored there. If the eighth parameter is not NULL, then the resulting time_t
is stored there.
Upvotes: 0
Reputation: 33666
mktime
[and localtime
] are non-trivial for all edge cases. They're also highly optimized, so you're unlikely to do better speedwise.
So, just use them [doing the fast fill of (e.g.) struct tm temp
you're already doing].
But, a speedup is to add time_t tod
to your struct. Fill it from mktime
once when you create your struct. This can save many repetitive/duplicate calls to mktime
.
You can even defer the mktime
call (i.e. only some of your structs may need it). Set tod
to a sentinel value (e.g. -2). When you actually need to use tod
, fill it from mktime
if tod
is the sentinel value
Upvotes: 3