-
Notifications
You must be signed in to change notification settings - Fork 5.4k
experiment with speeding up Time.local #13968
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
deefc8c
to
e2066b2
Compare
e2066b2
to
e8595b9
Compare
That benchmark looks including Rather, I feel that this problem should be reported to Apple. #include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
void
localtime_benchmark(const char *cond)
{
struct timespec ts, te;
clock_gettime(CLOCK_MONOTONIC , &ts);
for (long n = 0; n < 10000; ++n) {
time_t lt = 1753075380;
struct tm t;
localtime_r(<, &t);
}
clock_gettime(CLOCK_MONOTONIC , &te);
time_t diff = te.tv_sec - ts.tv_sec;
unsigned long ndiff = te.tv_nsec;
if (te.tv_nsec < ts.tv_nsec) {
--diff;
ndiff += 1000000000UL;
}
ndiff -= ts.tv_nsec;
printf("%s: %ld.%.9lu\n", cond, diff, ndiff);
}
int
main(void)
{
localtime_benchmark("non-forked");
pid_t forked = fork();
if (forked) {
int stat;
waitpid(forked, &stat, 0);
}
else {
localtime_benchmark(" forked");
}
return 0;
}
|
time.c
Outdated
/* Days in each month (non-leap year) */ | ||
static const int days_in_month[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; | ||
|
||
/* Check if year is a leap year */ | ||
static inline int | ||
is_leap_year(int year) | ||
{ | ||
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); | ||
} | ||
|
||
/* Get days in month, accounting for leap years */ | ||
static inline int | ||
get_days_in_month(int year, int month) | ||
{ | ||
if (month == 2 && is_leap_year(year)) { | ||
return 29; | ||
} | ||
return days_in_month[month - 1]; | ||
} | ||
|
||
/* Calculate day of week from year/month/day */ | ||
static int | ||
calculate_wday(int year, int month, int day) | ||
{ | ||
/* Zeller's congruence algorithm */ | ||
if (month < 3) { | ||
month += 12; | ||
year--; | ||
} | ||
int k = year % 100; | ||
int j = year / 100; | ||
int h = (day + (13 * (month + 1)) / 5 + k + k / 4 + j / 4 - 2 * j) % 7; | ||
/* Convert to tm_wday format (Sunday = 0) */ | ||
return (h + 6) % 7; | ||
} | ||
|
||
/* Calculate day of year from year/month/day */ | ||
static int | ||
calculate_yday(int year, int month, int day) | ||
{ | ||
int yday = 0; | ||
for (int m = 1; m < month; m++) { | ||
yday += get_days_in_month(year, m); | ||
} | ||
return yday + day - 1; /* tm_yday is 0-based */ | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These look like duplicates to the existing functions.
Indeed. I did some searching around and found a few references which make me think this won't be fixed: https://developer.apple.com/forums/thread/747499 And from fork's manpage:
AIUI there is IPC signaling in place to notify processes of timezone changes, and these don't work after fork() before exec(). |
time.c
Outdated
/* Calculate day of week from year/month/day */ | ||
static int | ||
calculate_wday(int year, int month, int day) | ||
{ | ||
/* Zeller's congruence algorithm */ | ||
if (month < 3) { | ||
month += 12; | ||
year--; | ||
} | ||
int k = year % 100; | ||
int j = year / 100; | ||
int h = (day + (13 * (month + 1)) / 5 + k + k / 4 + j / 4 - 2 * j) % 7; | ||
/* Convert to tm_wday format (Sunday = 0) */ | ||
return (h + 6) % 7; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can use calc_wday
.
@@ -703,6 +704,11 @@ static struct vtm *localtimew(wideval_t timew, struct vtm *result); | |||
|
|||
static int leap_year_p(long y); | |||
#define leap_year_v_p(y) leap_year_p(NUM2LONG(modv((y), INT2FIX(400)))) | |||
static int calc_tm_yday(long tm_year, int tm_mon, int tm_mday); | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
static int calc_wday(int year_mod400, int month, int day); | |
time.c
Outdated
tm->tm_mday = day; | ||
|
||
/* Recalculate wday and yday */ | ||
tm->tm_wday = calculate_wday(year, month, day); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tm->tm_wday = calculate_wday(year, month, day); | |
tm->tm_wday = calc_wday(year % 400, month, day); |
/* Cache key: 15-minute precision */ | ||
typedef struct { | ||
int year; | ||
int month; | ||
int day; | ||
int hour; | ||
int quarter; /* 0-3 representing 00, 15, 30, 45 minute intervals */ | ||
} offset_cache_key; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess it would be possible to pack the cache keys, so that the validness can be checked atomically.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great idea. I think now we can probably just use our time_t value/15 for our cache key?
I haven't quite convinced myself that it's safe to use 15 minute quarters to cache by..... There are some historical timezones with odd offsets. I don't know of any yet where a dst boundary doesn't happen on a 15 minute boundary in UTC time though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps modern time zones do not have such odd UTC offsets, but "zdump(1)" refers to "Europe/Astrahan".
$ LC_TIME=C /bin/date -z Europe/Astrakhan -j {-f,+}%Y-%m-%dT%H:%M:%S%Z 1924-04-30T00:00:00GMT
1924-04-30T03:12:12LMT
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it'd be OK to abandon such cases, i.e., do not cache if the gmtoff % (15 * 60) != 0
.
AC_ARG_ENABLE(macos-localtime-cache, | ||
AS_HELP_STRING([--enable-macos-localtime-cache], | ||
[enable localtime cache optimization for macOS to improve forked process performance]), | ||
[macos_localtime_cache=$enableval], [macos_localtime_cache=no]) | ||
AS_IF([test "$macos_localtime_cache" = yes], [ | ||
AS_CASE(["$target_os"], | ||
[darwin*], [ | ||
AC_DEFINE(ENABLE_MACOS_LOCALTIME_CACHE, 1, [Enable macOS localtime cache optimization]) | ||
AC_MSG_NOTICE([macOS localtime cache optimization enabled]) | ||
], | ||
[AC_MSG_WARN([--enable-macos-localtime-cache is only supported on macOS, ignoring])] | ||
) | ||
]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This may not be limited to macOS in the future.
And do you have any reason not to enable it on macOS by the default?
AC_ARG_ENABLE(macos-localtime-cache, | |
AS_HELP_STRING([--enable-macos-localtime-cache], | |
[enable localtime cache optimization for macOS to improve forked process performance]), | |
[macos_localtime_cache=$enableval], [macos_localtime_cache=no]) | |
AS_IF([test "$macos_localtime_cache" = yes], [ | |
AS_CASE(["$target_os"], | |
[darwin*], [ | |
AC_DEFINE(ENABLE_MACOS_LOCALTIME_CACHE, 1, [Enable macOS localtime cache optimization]) | |
AC_MSG_NOTICE([macOS localtime cache optimization enabled]) | |
], | |
[AC_MSG_WARN([--enable-macos-localtime-cache is only supported on macOS, ignoring])] | |
) | |
]) | |
AC_ARG_ENABLE(localtime-cache, | |
AS_HELP_STRING([--enable-localtime-cache], | |
[enable localtime cache optimization to improve forked process performance]), | |
[localtime_cache=$enableval], | |
[AS_CASE(["$target_os"], [darwin*], [localtime_cache=yes], [localtime_cache=no])]) | |
AS_IF([test "$localtime_cache" = yes], [ | |
AC_DEFINE(ENABLE_LOCALTIME_CACHE, 1, [Enable localtime cache optimization]) | |
AC_MSG_NOTICE([localtime cache optimization enabled]) | |
]) |
On macOS, methods like
Time.local
can be up to hundreds of times slower when run in a forked child process than when run in the parent process. This simple benchmark demonstrates the issue:This impacts all kinds of things from logging, parsing times, working with zipfiles with
rubyzip
, etc.This PR implements a small cache for offsets from UTC for any given 15-minute quarters derived from the value passed to
rb_localtime_r
- this avoids multiple calls to libc'slocaltime_r
for the same time values within the same 15-minute block of time. As far as I can tell there are no timezones past or present which have an offset that doesn't fall on a 15-minute boundary (e.g. +0/15/30/45 minutes).The cache is cleared if the timezone is changed.