Skip to content
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

30% optimization of DateTime.GetDate()/.Year/.Month/.Day/.DayOfYear by 'Euclidean affine functions' #72712

Merged
merged 9 commits into from
Jul 31, 2022
117 changes: 42 additions & 75 deletions src/libraries/System.Private.CoreLib/src/System/DateTime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public readonly partial struct DateTime
private const long TicksPerSecond = TicksPerMillisecond * 1000;
private const long TicksPerMinute = TicksPerSecond * 60;
private const long TicksPerHour = TicksPerMinute * 60;
private const ulong TicksPer6Hours = TicksPerHour * 6;
private const long TicksPerDay = TicksPerHour * 24;

// Number of milliseconds per time unit
Expand Down Expand Up @@ -1351,92 +1352,58 @@ public DateTime Date

// Returns a given date part of this DateTime. This method is used
// to compute the year, day-of-year, month, or day part.
//
// Implementation based on article https://arxiv.org/pdf/2102.06959.pdf
// Cassio Neri, Lorenz Schneiderhttps - Euclidean Affine Functions and Applications to Calendar Algorithms - 2021
private int GetDatePart(int part)
{
// n = number of days since 1/1/0001
uint n = (uint)(UTicks / TicksPerDay);
// y400 = number of whole 400-year periods since 1/1/0001
uint y400 = n / DaysPer400Years;
// n = day number within 400-year period
n -= y400 * DaysPer400Years;
// y100 = number of whole 100-year periods within 400-year period
uint y100 = n / DaysPer100Years;
// Last 100-year period has an extra day, so decrement result if 4
if (y100 == 4) y100 = 3;
// n = day number within 100-year period
n -= y100 * DaysPer100Years;
// y4 = number of whole 4-year periods within 100-year period
uint y4 = n / DaysPer4Years;
// n = day number within 4-year period
n -= y4 * DaysPer4Years;
// y1 = number of whole years within 4-year period
uint y1 = n / DaysPerYear;
// Last year has an extra day, so decrement result if 4
if (y1 == 4) y1 = 3;
// If year was requested, compute and return it
if (part == DatePartYear)
// y400 = number of whole 400-year periods since 3/1/0000
// r1 = day number within 400-year period
(uint y400, uint r1) = Math.DivRem(((uint)(UTicks / TicksPer6Hours) | 3U) + 1224, DaysPer400Years);
ulong u2 = (ulong)Math.BigMul(2939745, (int)r1 | 3);
ushort daySinceMarch1 = (ushort)((uint)u2 / 11758980);
var n3 = 2141 * daySinceMarch1 + 197913;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... Some additional comments, especially around the specific magic numbers, would be greatly appreciated.

Copy link
Contributor Author

@SergeiPavlov SergeiPavlov Jul 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These magic number have not explainable meaning. They just give right result.
To understand them it is neccessary to follow chain of propositions in the article.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then you should at least be noting the source inside the code. If there is any sort of naming for those constants in the source, you should probably be pulling them out into named constants as well.

Copy link
Contributor Author

@SergeiPavlov SergeiPavlov Jul 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did in the function comment https://github.com/dotnet/runtime/pull/72712/files#diff-5afffb30d8c00fa852b7df6e5bce6255be93acc1281d20b308ab57e8b70f567bR1356

Authors of the algorithm don't use any constant naming in the article and their reference implementation https://github.com/cassioneri/calendar/blob/master/calendar.hpp#L285

switch (part)
{
case DatePartDay:
return (ushort)n3 / 2141 + 1;
case DatePartMonth:
return (ushort)(n3 >> 16) - (daySinceMarch1 >= 306 ? 12 : 0);
}

var year = (int)(100 * y400 + (uint)(u2 >> 32)) + (daySinceMarch1 >= 306 ? 1 : 0);
switch (part)
SergeiPavlov marked this conversation as resolved.
Show resolved Hide resolved
{
return (int)(y400 * 400 + y100 * 100 + y4 * 4 + y1 + 1);
case DatePartYear:
return year;
default: // DatePartDayOfYear
return daySinceMarch1 >= 306
? daySinceMarch1 - 305 // rollover December 31
: daySinceMarch1 + 60 + (IsLeapYear(year) ? 1 : 0);
}
// n = day number within year
n -= y1 * DaysPerYear;
// If day-of-year was requested, return it
if (part == DatePartDayOfYear) return (int)n + 1;
// Leap year calculation looks different from IsLeapYear since y1, y4,
// and y100 are relative to year 1, not year 0
uint[] days = y1 == 3 && (y4 != 24 || y100 == 3) ? s_daysToMonth366 : s_daysToMonth365;
// All months have less than 32 days, so n >> 5 is a good conservative
// estimate for the month
uint m = (n >> 5) + 1;
// m = 1-based month number
while (n >= days[m]) m++;
// If month was requested, return it
if (part == DatePartMonth) return (int)m;
// Return 1-based day-of-month
return (int)(n - days[m - 1] + 1);
}

// Exactly the same as GetDatePart, except computing all of
// year/month/day rather than just one of them. Used when all three
// are needed rather than redoing the computations for each.
internal void GetDate(out int year, out int month, out int day)
{
// n = number of days since 1/1/0001
uint n = (uint)(UTicks / TicksPerDay);
// y400 = number of whole 400-year periods since 1/1/0001
uint y400 = n / DaysPer400Years;
// n = day number within 400-year period
n -= y400 * DaysPer400Years;
// y100 = number of whole 100-year periods within 400-year period
uint y100 = n / DaysPer100Years;
// Last 100-year period has an extra day, so decrement result if 4
if (y100 == 4) y100 = 3;
// n = day number within 100-year period
n -= y100 * DaysPer100Years;
// y4 = number of whole 4-year periods within 100-year period
uint y4 = n / DaysPer4Years;
// n = day number within 4-year period
n -= y4 * DaysPer4Years;
// y1 = number of whole years within 4-year period
uint y1 = n / DaysPerYear;
// Last year has an extra day, so decrement result if 4
if (y1 == 4) y1 = 3;
// compute year
year = (int)(y400 * 400 + y100 * 100 + y4 * 4 + y1 + 1);
// n = day number within year
n -= y1 * DaysPerYear;
// dayOfYear = n + 1;
// Leap year calculation looks different from IsLeapYear since y1, y4,
// and y100 are relative to year 1, not year 0
uint[] days = y1 == 3 && (y4 != 24 || y100 == 3) ? s_daysToMonth366 : s_daysToMonth365;
// All months have less than 32 days, so n >> 5 is a good conservative
// estimate for the month
uint m = (n >> 5) + 1;
// m = 1-based month number
while (n >= days[m]) m++;
// compute month and day
month = (int)m;
day = (int)(n - days[m - 1] + 1);
// y400 = number of whole 400-year periods since 3/1/0000
// r1 = day number within 400-year period
(uint y400, uint r1) = Math.DivRem(((uint)(UTicks / TicksPer6Hours) | 3U) + 1224, DaysPer400Years);
ulong u2 = (ulong)Math.BigMul(2939745, (int)r1 | 3);
ushort daySinceMarch1 = (ushort)((uint)u2 / 11758980);
var n3 = 2141 * daySinceMarch1 + 197913;
SergeiPavlov marked this conversation as resolved.
Show resolved Hide resolved
year = (int)(100 * y400 + (uint)(u2 >> 32));
month = (ushort)(n3 >> 16);
day = (ushort)n3 / 2141 + 1;

// rollover December 31
if (daySinceMarch1 >= 306)
{
++year;
month -= 12;
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
Expand Down