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

Optimize DateOnly properties and deconstruct #103352

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 61 additions & 7 deletions src/libraries/System.Private.CoreLib/src/System/DateOnly.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ public readonly struct DateOnly
// Maps to Jan 1st year 1
private const int MinDayNumber = 0;

// Maps to December 31 year 9999. The value calculated from "new DateTime(9999, 12, 31).Ticks / TimeSpan.TicksPerDay"
private const int MaxDayNumber = 3_652_058;
// Maps to December 31 year 9999.
private const int MaxDayNumber = DateTime.DaysTo10000 - 1;

private static int DayNumberFromDateTime(DateTime dt) => (int)((ulong)dt.Ticks / TimeSpan.TicksPerDay);

Expand Down Expand Up @@ -83,17 +83,49 @@ public static DateOnly FromDayNumber(int dayNumber)
/// <summary>
/// Gets the year component of the date represented by this instance.
/// </summary>
public int Year => GetEquivalentDateTime().Year;
public int Year
{
get
{
// y100 = number of whole 100-year periods since 1/1/0001
// r1 = (day number within 100-year period) * 4
(uint y100, uint r1) = Math.DivRem((((uint)_dayNumber << 2) | 3U), DateTime.DaysPer400Years);
return 1 + (int)(100 * y100 + (r1 | 3) / DateTime.DaysPer4Years);
}
}

/// <summary>
/// Gets the month component of the date represented by this instance.
/// </summary>
public int Month => GetEquivalentDateTime().Month;
public int Month
{
get
{
// r1 = (day number within 100-year period) * 4
uint r1 = ((((uint)_dayNumber << 2) | 3U) + 1224) % DateTime.DaysPer400Years;
ulong u2 = Math.BigMul(DateTime.EafMultiplier, r1 | 3U);
ushort daySinceMarch1 = (ushort)((uint)u2 / DateTime.EafDivider);
int n3 = 2141 * daySinceMarch1 + 197913;
return (ushort)(n3 >> 16) - (daySinceMarch1 >= DateTime.March1BasedDayOfNewYear ? 12 : 0);
}
}

/// <summary>
/// Gets the day component of the date represented by this instance.
/// </summary>
public int Day => GetEquivalentDateTime().Day;
public int Day
{
get
{
// r1 = (day number within 100-year period) * 4
uint r1 = ((((uint)_dayNumber << 2) | 3U) + 1224) % DateTime.DaysPer400Years;
ulong u2 = Math.BigMul(DateTime.EafMultiplier, r1 | 3U);
ushort daySinceMarch1 = (ushort)((uint)u2 / DateTime.EafDivider);
int n3 = 2141 * daySinceMarch1 + 197913;
// Return 1-based day-of-month
return (ushort)n3 / 2141 + 1;
Copy link
Member

@EgorBo EgorBo Jun 12, 2024

Choose a reason for hiding this comment

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

here and in above - don't you just effectively inline DateTime.Day/Month/Year by hands? Shouldn't it be JIT's resposobility?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In effect it circumvents 64-bit multiplication, 64-bit bitwise-and, 64-bit division; and replaces it with a 32-bit shift.

Should we assume JIT takes care of that?

Copy link
Member

Choose a reason for hiding this comment

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

I agree with @EgorBo. I don't think we need to complicate the code just to do what the JIT can do. I am concerned about the code maintainability. Keeping it simple is easier to touch in the future.

}
}

/// <summary>
/// Gets the day of the week represented by this instance.
Expand All @@ -103,7 +135,8 @@ public static DateOnly FromDayNumber(int dayNumber)
/// <summary>
/// Gets the day of the year represented by this instance.
/// </summary>
public int DayOfYear => GetEquivalentDateTime().DayOfYear;
public int DayOfYear
=> 1 + (int)((((((uint)_dayNumber << 2) | 3U) % (uint)DateTime.DaysPer400Years) | 3U) * DateTime.EafMultiplier / DateTime.EafDivider);

/// <summary>
/// Gets the number of days since January 1, 0001 in the Proleptic Gregorian calendar represented by this instance.
Expand Down Expand Up @@ -207,7 +240,28 @@ public DateOnly AddDays(int value)
/// </param>
[EditorBrowsable(EditorBrowsableState.Never)]
public void Deconstruct(out int year, out int month, out int day)
=> GetEquivalentDateTime().GetDate(out year, out month, out day);
{
// Implementation based on article https://arxiv.org/pdf/2102.06959.pdf
// Cassio Neri, Lorenz Schneider - Euclidean Affine Functions and Applications to Calendar Algorithms - 2021

// y100 = number of whole 100-year periods since 3/1/0000
// r1 = (day number within 100-year period) * 4
(uint y100, uint r1) = Math.DivRem((((uint)_dayNumber << 2) | 3U) + 1224, DateTime.DaysPer400Years);
ulong u2 = Math.BigMul(DateTime.EafMultiplier, r1 | 3U);
ushort daySinceMarch1 = (ushort)((uint)u2 / DateTime.EafDivider);
int n3 = 2141 * daySinceMarch1 + 197913;
year = (int)(100 * y100 + (uint)(u2 >> 32));
// compute month and day
month = (ushort)(n3 >> 16);
day = (ushort)n3 / 2141 + 1;

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

/// <summary>
/// Returns a DateTime that is set to the date of this DateOnly instance and the time of specified input time.
Expand Down
12 changes: 6 additions & 6 deletions src/libraries/System.Private.CoreLib/src/System/DateTime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,11 @@ public readonly partial struct DateTime
// Number of days in a non-leap year
private const int DaysPerYear = 365;
// Number of days in 4 years
private const int DaysPer4Years = DaysPerYear * 4 + 1; // 1461
internal const int DaysPer4Years = DaysPerYear * 4 + 1; // 1461
// Number of days in 100 years
private const int DaysPer100Years = DaysPer4Years * 25 - 1; // 36524
// Number of days in 400 years
private const int DaysPer400Years = DaysPer100Years * 4 + 1; // 146097
internal const int DaysPer400Years = DaysPer100Years * 4 + 1; // 146097

// Number of days from 1/1/0001 to 12/31/1600
private const int DaysTo1601 = DaysPer400Years * 4; // 584388
Expand All @@ -89,7 +89,7 @@ public readonly partial struct DateTime
// Number of days from 1/1/0001 to 12/31/1969
internal const int DaysTo1970 = DaysPer400Years * 4 + DaysPer100Years * 3 + DaysPer4Years * 17 + DaysPerYear; // 719,162
// Number of days from 1/1/0001 to 12/31/9999
private const int DaysTo10000 = DaysPer400Years * 25 - 366; // 3652059
internal const int DaysTo10000 = DaysPer400Years * 25 - 366; // 3652059

internal const long MinTicks = 0;
internal const long MaxTicks = DaysTo10000 * TicksPerDay - 1;
Expand All @@ -116,11 +116,11 @@ public readonly partial struct DateTime
// Constants used for fast calculation of following subexpressions
// x / DaysPer4Years
// x % DaysPer4Years / 4
private const uint EafMultiplier = (uint)(((1UL << 32) + DaysPer4Years - 1) / DaysPer4Years); // 2,939,745
private const uint EafDivider = EafMultiplier * 4; // 11,758,980
internal const uint EafMultiplier = (uint)(((1UL << 32) + DaysPer4Years - 1) / DaysPer4Years); // 2,939,745
internal const uint EafDivider = EafMultiplier * 4; // 11,758,980

private const ulong TicksPer6Hours = TicksPerHour * 6;
private const int March1BasedDayOfNewYear = 306; // Days between March 1 and January 1
internal const int March1BasedDayOfNewYear = 306; // Days between March 1 and January 1

internal static ReadOnlySpan<uint> DaysToMonth365 => [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365];
internal static ReadOnlySpan<uint> DaysToMonth366 => [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,7 @@ public abstract class Calendar : ICloneable
internal const int MillisPerHour = MillisPerMinute * 60;
internal const int MillisPerDay = MillisPerHour * 24;

// Number of days in a non-leap year
internal const int DaysPerYear = 365;
// Number of days in 4 years
internal const int DaysPer4Years = DaysPerYear * 4 + 1;
// Number of days in 100 years
internal const int DaysPer100Years = DaysPer4Years * 25 - 1;
// Number of days in 400 years
internal const int DaysPer400Years = DaysPer100Years * 4 + 1;

// Number of days from 1/1/0001 to 1/1/10000
internal const int DaysTo10000 = DaysPer400Years * 25 - 366;

internal const long MaxMillis = (long)DaysTo10000 * MillisPerDay;
internal const long MaxMillis = (long)DateTime.DaysTo10000 * MillisPerDay;

private int _currentEraValue = -1;

Expand Down
Loading