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
168 changes: 77 additions & 91 deletions src/libraries/System.Private.CoreLib/src/System/DateTime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,9 @@ public readonly partial struct DateTime
// All OA dates must be less than (not <=) OADateMaxAsDouble
private const double OADateMaxAsDouble = 2958466.0;

private const int DatePartYear = 0;
private const int DatePartDayOfYear = 1;
private const int DatePartMonth = 2;
private const int DatePartDay = 3;
// Euclidean Affine Functions Algorithm constants
private const ulong TicksPer6Hours = TicksPerHour * 6;
private const int March1BasedDayOfNewYear = 306; // Days between March 1 and January 1

private static readonly uint[] s_daysToMonth365 = {
0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 };
Expand Down Expand Up @@ -1349,94 +1348,31 @@ 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.
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)
{
return (int)(y400 * 400 + y100 * 100 + y4 * 4 + y1 + 1);
}
// 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
// Exactly the same as Year, Month, Day properties, 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.
//
// Implementation based on article https://arxiv.org/pdf/2102.06959.pdf
// Cassio Neri, Lorenz Schneiderhttps - Euclidean Affine Functions and Applications to Calendar Algorithms - 2021
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++;
// 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);
int n3 = 2141 * daySinceMarch1 + 197913;
year = (int)(100 * y400 + (uint)(u2 >> 32));
// compute month and day
month = (int)m;
day = (int)(n - days[m - 1] + 1);
month = (ushort)(n3 >> 16);
day = (ushort)n3 / 2141 + 1;

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

[MethodImpl(MethodImplOptions.AggressiveInlining)]
Expand Down Expand Up @@ -1479,7 +1415,19 @@ internal void GetTimePrecise(out int hour, out int minute, out int second, out i
// Returns the day-of-month part of this DateTime. The returned
// value is an integer between 1 and 31.
//
public int Day => GetDatePart(DatePartDay);
public int Day
{
get
{
// r1 = day number within 400-year period
uint r1 = (((uint)(UTicks / TicksPer6Hours) | 3U) + 1224) % DaysPer400Years;
ulong u2 = (ulong)Math.BigMul(2939745, (int)r1 | 3);
ushort daySinceMarch1 = (ushort)((uint)u2 / 11758980);
int n3 = 2141 * daySinceMarch1 + 197913;
// Return 1-based day-of-month
return (ushort)n3 / 2141 + 1;
}
}

// Returns the day-of-week part of this DateTime. The returned value
// is an integer between 0 and 6, where 0 indicates Sunday, 1 indicates
Expand All @@ -1491,7 +1439,22 @@ internal void GetTimePrecise(out int hour, out int minute, out int second, out i
// Returns the day-of-year part of this DateTime. The returned value
// is an integer between 1 and 366.
//
public int DayOfYear => GetDatePart(DatePartDayOfYear);
public int DayOfYear
{
get
{
// 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);

int year = (int)(100 * y400 + (uint)(u2 >> 32)) + (daySinceMarch1 >= March1BasedDayOfNewYear ? 1 : 0);
return daySinceMarch1 >= March1BasedDayOfNewYear // DatePartDayOfYear case
? daySinceMarch1 - March1BasedDayOfNewYear + 1 // rollover December 31
: daySinceMarch1 + (366 - March1BasedDayOfNewYear) + (IsLeapYear(year) ? 1 : 0);
}
}

// Returns the hash code for this DateTime.
//
Expand Down Expand Up @@ -1543,7 +1506,18 @@ public DateTimeKind Kind
// Returns the month part of this DateTime. The returned value is an
// integer between 1 and 12.
//
public int Month => GetDatePart(DatePartMonth);
public int Month
{
get
{
// r1 = day number within 400-year period
uint r1 = (((uint)(UTicks / TicksPer6Hours) | 3U) + 1224) % DaysPer400Years;
ulong u2 = (ulong)Math.BigMul(2939745, (int)r1 | 3);
ushort daySinceMarch1 = (ushort)((uint)u2 / 11758980);
int n3 = 2141 * daySinceMarch1 + 197913;
return (ushort)(n3 >> 16) - (daySinceMarch1 >= March1BasedDayOfNewYear ? 12 : 0);
}
}

// Returns a DateTime representing the current date and time. The
// resolution of the returned value depends on the system timer.
Expand Down Expand Up @@ -1591,7 +1565,19 @@ public static DateTime Now
// Returns the year part of this DateTime. The returned value is an
// integer between 1 and 9999.
//
public int Year => GetDatePart(DatePartYear);
public int Year
{
get
{
// 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);

return (int)(100 * y400 + (uint)(u2 >> 32)) + (daySinceMarch1 >= March1BasedDayOfNewYear ? 1 : 0);
}
}

// Checks whether a given year is a leap year. This method returns true if
// year is a leap year, or false if not.
Expand Down