-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Optimize DateTimeOffset #111112
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 DateTimeOffset #111112
Conversation
|
Tagging subscribers to this area: @dotnet/area-system-datetime |
huoyaoyuan
left a comment
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 is really questionable to me. It's employing a lot of anti-patterns to workaround a maybe-realistic codegen limitation as mentioned in #58169. Do you have any measured performance data to support it? Moreover, the codegen limitations are better solved in JIT, instead of workarounded in managed code.
|
|
||
| [MethodImpl(MethodImplOptions.NoInlining)] | ||
| internal static unsafe bool IsValidTimeWithLeapSeconds(int year, int month, int day, int hour, int minute, DateTimeKind kind) | ||
| private static unsafe bool IsValidTimeWithLeapSeconds(DateTime value) |
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 is likely a deoptimization. Decomposing DateTime has its overhead.
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 method is never called in practice. And hopefully there will be no more leap seconds added in the future.
| public DateTime AddMonths(int months) => AddMonths(this, months); | ||
| private static DateTime AddMonths(DateTime date, int months) |
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 is not a good practice. The static method doesn't save anything.
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.
The calling convention for struct instance methods always forces the struct on stack, unless the method is inlined.
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 see the problem in calling convention. Is there any measurement about the performance impact?
| _dateData >> KindShift == KindLocalAmbiguousDst >> KindShift; | ||
|
|
||
| public DateTimeKind Kind | ||
| { | ||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| get => InternalKind switch | ||
| get => (_dateData >> KindShift) switch |
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.
It's anti-pattern to operate on raw bits rather than reusable properties.
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 is the reusable property (InternalKind is inefficient for this purpose).
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.
InternalKind is expected to be efficient. What's the problem here? It should always be inlined.
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.
Using shift instead of mask produces smaller code (due to the large values of FlagsMask, etc.).
I measured 3 different common scenarios using 3 different approaches:
| BASELINE | Mean | Error | StdDev |
|---|---|---|---|
| DateTime_Kind | 0.9101 ns | 0.0050 ns | 0.0030 ns |
| DateTime_KindBranch | 0.6964 ns | 0.0093 ns | 0.0055 ns |
| DateTime_KindSwitch | 2.6310 ns | 0.2747 ns | 0.1817 ns |
| SHIFT | Mean | Error | StdDev |
|---|---|---|---|
| DateTime_Kind | 0.9214 ns | 0.0087 ns | 0.0058 ns |
| DateTime_KindBranch | 0.4721 ns | 0.0038 ns | 0.0025 ns |
| DateTime_KindSwitch | 2.0659 ns | 0.0146 ns | 0.0087 ns |
| BRANCHLESS | Mean | Error | StdDev |
|---|---|---|---|
| DateTime_Kind | 0.4690 ns | 0.0019 ns | 0.0010 ns |
| DateTime_KindBranch | 0.5248 ns | 0.0039 ns | 0.0026 ns |
| DateTime_KindSwitch | 0.7505 ns | 0.0083 ns | 0.0055 ns |
Bechmark source (note that the branching versions rely on perfect branch prediction)
int _ops = 1000;
DateTime _empty;
DateTime _utcNow = DateTime.UtcNow;
[Benchmark(OperationsPerInvoke = 1000)]
public int DateTime_Kind()
{
int res = 0;
int c = _ops;
DateTime value = _empty, next = _utcNow;
do
{
res += (int)value.Kind;
value = next;
} while (--c > 0);
return res;
}
[Benchmark(OperationsPerInvoke = 1000)]
public int DateTime_KindBranch()
{
int res = 0;
int c = _ops;
DateTime value = _empty, next = _utcNow;
do
{
if (value.Kind == DateTimeKind.Utc) res++;
value = next;
} while (--c > 0);
return res;
}
[Benchmark(OperationsPerInvoke = 1000)]
public int DateTime_KindSwitch()
{
int res = 0;
int c = _ops;
DateTime value = _empty, next = _utcNow;
do
{
switch (value.Kind)
{
case DateTimeKind.Unspecified: res += 7; break;
case DateTimeKind.Utc: res += 9; break;
default: res += 3; break;
}
value = next;
} while (--c > 0);
return res;
}
src/libraries/System.Private.CoreLib/src/System/Globalization/CultureInfo.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs
Outdated
Show resolved
Hide resolved
| private static readonly long s_minDateTicks = DateTime.MinValue.Ticks; | ||
| private static readonly long s_maxDateTicks = DateTime.MaxValue.Ticks; | ||
|
|
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.
readonly statics are JIT-time constants, removing this doesn't improve any performance.
class C
{
static readonly long s_minDateTicks = DateTime.MinValue.Ticks;
static readonly long s_maxDateTicks = DateTime.MaxValue.Ticks;
[MethodImpl(MethodImplOptions.NoInlining)]
static long GetMinTicks() => s_minDateTicks;
[MethodImpl(MethodImplOptions.NoInlining)]
static long GetMaxTicks() => s_maxDateTicks;
}Tier-1 codegen for GetMinTicks and GetMaxTicks:
; Assembly listing for method C:GetMinTicks():long (Tier1)
G_M18344_IG01: ;; offset=0x0000
;; size=0 bbWeight=1 PerfScore 0.00
G_M18344_IG02: ;; offset=0x0000
xor eax, eax
;; size=2 bbWeight=1 PerfScore 0.25
G_M18344_IG03: ;; offset=0x0002
ret
;; size=1 bbWeight=1 PerfScore 1.00
; Assembly listing for method C:GetMaxTicks():long (Tier1)
G_M12726_IG01: ;; offset=0x0000
;; size=0 bbWeight=1 PerfScore 0.00
G_M12726_IG02: ;; offset=0x0000
mov rax, 0x2BCA2875F4373FFF
;; size=10 bbWeight=1 PerfScore 0.25
G_M12726_IG03: ;; offset=0x000A
ret
;; size=1 bbWeight=1 PerfScore 1.00There 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.
They are not guaranteed to be JIT time constants and only work well with tiered compilation. New code should try to avoid such fields for constant values.
Previous discussion: #58169 (comment)
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.
For runtime doesn't run tiered compilation like NativeAOT, static readonly fields like the above are always preinitialized so this won't be a problem.
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 readonly fields like the above are always preinitialized so this won't be a problem.
That's not correct in this case today. DateTime.MaxValue.Ticks is too complicated expression for NAOT compiler to interpret at build time: https://godbolt.org/z/bdcErcEa6
| { | ||
| ulong ticks = calendar.ToDateTime(year, month, day, hour, minute, second, millisecond).UTicks; | ||
| _dateData = ticks | ((ulong)kind << KindShift); | ||
| _dateData = ticks | ((ulong)(uint)kind << KindShift); |
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.
If there's an improvement to be gained from the intermediate cast, shouldn't the JIT be able to just generate better code accordingly, given it knows from L290 that kind is in range of a uint?
cc: @EgorBo
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.
@stephentoub unfortunately, it's not yet handled by JIT for shifts (just checked), but it should be trivial to implement. Minimal repro. I'll take a look.
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.
It's not the shift but extension to ulong before shifting: repro should look like that.
| } | ||
|
|
||
| [MethodImpl(MethodImplOptions.NoInlining)] | ||
| private static ulong WithLeapSecond(Calendar calendar, int year, int month, int day, int hour, int minute, int millisecond, DateTimeKind kind) |
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.
What is the benefit of this manual outlining? Does the ctor get inlined automatically with this factored out but not with it left in line?
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.
The ctors are quite inlineable in both cases, but this just reduces the code size footprint of leap seconds even further: https://www.diffchecker.com/TbFmWwyz/
| // codeql[cs/leap-year/unsafe-date-construction-from-two-elements] - DateTime is constructed using the user specified values, not a combination of different sources. It would be intentional to throw if an invalid combination occurred. | ||
| this = new DateTime(year, month, day, hour, minute, 59); | ||
| ValidateLeapSecond(); | ||
| _dateData = WithLeapSecond(ticks, hour, minute); |
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.
Same question
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.
The ctors that don't use Calendar now take advantage of the fact that year/month/day (+kind) can be converted to ticks before leap seconds check, thus reducing the number of ctor variants that need to directly handle leap seconds and saving on code size: https://www.diffchecker.com/Iuqa8iGs/
| CurrentInfo; // Couldn't get anything, just use currentInfo as fallback | ||
| public static DateTimeFormatInfo GetInstance(IFormatProvider? provider) | ||
| { | ||
| return provider == null ? CurrentInfo : GetProviderNonNull(provider); |
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 appears to be optimizing for the case where provider is null at the expense of more code at the call site... why is that desirable?
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 took this approach from NumberFormatInfo which looks like it has been more thoroughly optimized. It adds a small code size increase on some callsites (but not all - in case of const provider = null it's an improvement).
|
Thanks. Can you share benchmarks/numbers? |
|
Some benchmarks based on real-world use case of
Benchmark code distilled from a codebase using IMemoryCacheICacheEntry _cacheEntry = new MemoryCache(new MemoryCacheOptions()).CreateEntry("test").SetAbsoluteExpiration(DateTimeOffset.UtcNow);
TimeSpan _defaultAbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30);
[Benchmark]
public void CacheEntry_SetAbsoluteExpiration() => _cacheEntry.AbsoluteExpiration = DateTimeOffset.UtcNow.Add(_defaultAbsoluteExpirationRelativeToNow);
[Benchmark]
public DateTime CacheEntry_GetAbsoluteExpiration() => _cacheEntry.AbsoluteExpiration.GetValueOrDefault().UtcDateTime; |
|
@pentp thanks for providing this change. Could you please provide the benchmark numbers for the touched public APIs? like the DateTime constructors, AddXXXX methods, etc. I am not expecting any surprises but want to ensure no regressions |
Both the constructors and About the struct calling convention - is this something that the runtime/JIT could automatically do for every int/pointer sized single-field readonly struct for non-virtual methods perhaps? There's the theoretical issue that someone could do |
@jkotas @EgorBo any thoughts about that? @stephentoub do you have any more feedback on this PR before completing it? |
|
Benchmarks for
Codegen diff is quite significant: https://www.diffchecker.com/qRujjOPj/ Benchmark codeint _ops = 1000;
DateTime _utcNow = DateTime.UtcNow;
[Benchmark(OperationsPerInvoke = 1000)]
public int Int32_TryFormat()
{
Span<char> buf = stackalloc char[24];
int c = _ops;
do
{
(-c).TryFormat(buf, out _, provider: CultureInfo.InvariantCulture);
} while (--c > 0);
return buf[0];
}
[Benchmark(OperationsPerInvoke = 1000)]
public int DateTime_TryFormat()
{
Span<char> buf = stackalloc char[24];
int c = _ops;
do
{
_utcNow.TryFormat(buf, out _, provider: CultureInfo.InvariantCulture);
} while (--c > 0);
return buf[0];
} |
|
I think the feedback is addressed for this PR. If we don't get any more feedback, I'll go ahead and merge it by the end of the day. |
|
Thanks @pentp for providing these changes. |
DateTimeOffset.DateTime.NumberFormatInfo.GetInstanceandDateTimeFormatInfo.GetInstance.DateTimeOffset.UtcNowto not haveKind=Utcin its_dateTimevalue (the existing assert didn't catch it because it was basically a no-op). Added better asserts.DateTime[Offset].Addressed feedback from the previous version of this PR: #58169