-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Improve performance of DateTime.UtcNow on Windows #44771
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
Changes from 1 commit
3e0d82d
c1c80e9
6589d69
834d783
d2a9f8a
35d60b8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | |||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -3,28 +3,139 @@ | ||||||||||||||||||||||
|
|||||||||||||||||||||||
using System.Runtime.CompilerServices; | |||||||||||||||||||||||
using System.Runtime.InteropServices; | |||||||||||||||||||||||
using System.Threading; | |||||||||||||||||||||||
|
|||||||||||||||||||||||
namespace System | |||||||||||||||||||||||
{ | |||||||||||||||||||||||
public readonly partial struct DateTime | |||||||||||||||||||||||
{ | |||||||||||||||||||||||
internal static readonly bool s_systemSupportsLeapSeconds = SystemSupportsLeapSeconds(); | |||||||||||||||||||||||
private static LeapSecondCache? s_leapSecondCache; | |||||||||||||||||||||||
|
|||||||||||||||||||||||
// Question: Can this be [Stdcall, SuppressGCTransition]? | |||||||||||||||||||||||
private static unsafe delegate* unmanaged[Stdcall]<ulong*, void> s_pfnGetSystemTimeAsFileTime = GetGetSystemTimeAsFileTimeFnPtr(); | |||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
How about writing a test that would be mocking this delegate using a reflection? I know it's hacky, but it would be great to have the edge cases covered by automated tests. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting idea, but I wonder how fragile this might be. There's probably a bunch of code within the runtime that relies on There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Usually we need to report the time exact as the system. It is up to the users/admins to play with the system time. It is same as if you change the system time zone when you are running. local time will be very different and can cause issues. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If it's not going to be modified for tests using reflection, then could mark it as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be nice to have these private reflection based testing hooks in debug/checked build only, so that we are not leaving performance on the table in the shipping bits. |
|||||||||||||||||||||||
|
|||||||||||||||||||||||
private static unsafe delegate* unmanaged[Stdcall]<ulong*, void> GetGetSystemTimeAsFileTimeFnPtr() | |||||||||||||||||||||||
{ | |||||||||||||||||||||||
IntPtr kernel32Lib = NativeLibrary.Load("kernel32.dll", typeof(DateTime).Assembly, DllImportSearchPath.System32); | |||||||||||||||||||||||
IntPtr pfnGetSystemTime = NativeLibrary.GetExport(kernel32Lib, "GetSystemTimeAsFileTime"); // will never fail | |||||||||||||||||||||||
|
|||||||||||||||||||||||
if (NativeLibrary.TryGetExport(kernel32Lib, "GetSystemTimePreciseAsFileTime", out IntPtr pfnGetSystemTimePrecise)) | |||||||||||||||||||||||
{ | |||||||||||||||||||||||
// If GetSystemTimePreciseAsFileTime exists, we'd like to use it. However, on | |||||||||||||||||||||||
// misconfigured systems, it's possible for the "precise" time to be inaccurate: | |||||||||||||||||||||||
// https://github.com/dotnet/runtime/issues/9014 | |||||||||||||||||||||||
// If it's inaccurate, though, we expect it to be wildly inaccurate, so as a | |||||||||||||||||||||||
// workaround/heuristic, we get both the "normal" and "precise" times, and as | |||||||||||||||||||||||
// long as they're close, we use the precise one. This workaround can be removed | |||||||||||||||||||||||
// when we better understand what's causing the drift and the issue is no longer | |||||||||||||||||||||||
// a problem or can be better worked around on all targeted OSes. | |||||||||||||||||||||||
|
|||||||||||||||||||||||
ulong filetimeStd, filetimePrecise; | |||||||||||||||||||||||
((delegate* unmanaged[Stdcall]<ulong*, void>)pfnGetSystemTime)(&filetimeStd); | |||||||||||||||||||||||
((delegate* unmanaged[Stdcall]<ulong*, void>)pfnGetSystemTimePrecise)(&filetimePrecise); | |||||||||||||||||||||||
|
|||||||||||||||||||||||
if (Math.Abs((long)(filetimeStd - filetimePrecise)) <= 100 * TicksPerMillisecond) | |||||||||||||||||||||||
{ | |||||||||||||||||||||||
pfnGetSystemTime = pfnGetSystemTimePrecise; // use the precise version | |||||||||||||||||||||||
} | |||||||||||||||||||||||
} | |||||||||||||||||||||||
|
|||||||||||||||||||||||
return (delegate* unmanaged[Stdcall]<ulong*, void>)pfnGetSystemTime; | |||||||||||||||||||||||
} | |||||||||||||||||||||||
|
|||||||||||||||||||||||
public static unsafe DateTime UtcNow | |||||||||||||||||||||||
{ | |||||||||||||||||||||||
get | |||||||||||||||||||||||
{ | |||||||||||||||||||||||
if (s_systemSupportsLeapSeconds) | |||||||||||||||||||||||
// The OS tick count and .NET's tick count are slightly different. The OS tick | |||||||||||||||||||||||
// count is the *absolute* number of 100-ns intervals which have elapsed since | |||||||||||||||||||||||
// January 1, 1601 (UTC). Due to leap second handling, the number of ticks per | |||||||||||||||||||||||
// day is variable. Dec. 30, 2016 had 864,000,000,000 ticks (a standard 24-hour | |||||||||||||||||||||||
// day), but Dec. 31, 2016 had 864,010,000,000 ticks due to leap second insertion. | |||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just to mention so far Windows doesn't carry any leap seconds in the systems by default. I think the leap seconds will start to show up in the future. |
|||||||||||||||||||||||
// In .NET, *every* day is assumed to have exactly 24 hours (864,000,000,000 ticks). | |||||||||||||||||||||||
// This means that per the OS, midnight Dec. 31, 2016 + 864 bn ticks = Dec. 31, 2016 23:59:60, | |||||||||||||||||||||||
// but per .NET, midnight Dec. 31, 2016 + 864 bn ticks = Jan. 1, 2017 00:00:00. | |||||||||||||||||||||||
// | |||||||||||||||||||||||
// We can query the OS and have it deconstruct the tick count into (yyyy-mm-dd hh:mm:ss), | |||||||||||||||||||||||
// constructing a new DateTime object from these components, but this is slow. | |||||||||||||||||||||||
// So instead we'll rely on the fact that leap seconds only ever adjust the day | |||||||||||||||||||||||
// by +1 or -1 second, and only at the very end of the day. That is, time rolls | |||||||||||||||||||||||
// 23:59:58 -> 00:00:00 (negative leap second) or 23:59:59 -> 23:59:60 (positive leap | |||||||||||||||||||||||
// second). Thus we assume that each day has at least 23 hr 59 min 59 sec, or | |||||||||||||||||||||||
// 863,990,000,000 ticks. | |||||||||||||||||||||||
// | |||||||||||||||||||||||
// We take advantage of this by caching what the OS believes the tick count is at | |||||||||||||||||||||||
// the beginning of the day vs. what .NET believes the tick count is at the beginning | |||||||||||||||||||||||
// of the day. When the OS returns a tick count to us, if it's within 23:59:59 of | |||||||||||||||||||||||
// what midnight was on the current day, then we know that there's no way for a leap | |||||||||||||||||||||||
// second to have been inserted or removed, and we can short-circuit the leap second | |||||||||||||||||||||||
// handling logic by performing a quick addition and returning immediately. If the | |||||||||||||||||||||||
// OS-provided tick count is outside of our cached range, we'll update the cache. | |||||||||||||||||||||||
// On the off-chance the API is called on the very last second (or two) of the day, | |||||||||||||||||||||||
// we'll go down the slow path without updating the cache, and once another second | |||||||||||||||||||||||
// elapses we'll be able to update the cache again. | |||||||||||||||||||||||
|
|||||||||||||||||||||||
const long MinTicksPerDay = TicksPerDay - TicksPerSecond; // a day is at least 23:59:59 long | |||||||||||||||||||||||
|
|||||||||||||||||||||||
ulong osTicks; | |||||||||||||||||||||||
s_pfnGetSystemTimeAsFileTime(&osTicks); | |||||||||||||||||||||||
|
|||||||||||||||||||||||
// If the OS doesn't support leap second handling, short-circuit everything. | |||||||||||||||||||||||
|
|||||||||||||||||||||||
if (!s_systemSupportsLeapSeconds) | |||||||||||||||||||||||
{ | |||||||||||||||||||||||
FullSystemTime time; | |||||||||||||||||||||||
GetSystemTimeWithLeapSecondsHandling(&time); | |||||||||||||||||||||||
return CreateDateTimeFromSystemTime(in time); | |||||||||||||||||||||||
return new DateTime((osTicks + FileTimeOffset) | KindUtc); | |||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could write as |
|||||||||||||||||||||||
} | |||||||||||||||||||||||
|
|||||||||||||||||||||||
return new DateTime(((ulong)(GetSystemTimeAsFileTime() + FileTimeOffset)) | KindUtc); | |||||||||||||||||||||||
// If it's between 00:00:00 (inclusive) and 23:59:59 (exclusive) on the same day | |||||||||||||||||||||||
// as the previous call to UtcNow, we can use the cached value. We can't remove | |||||||||||||||||||||||
// the "is it before midnight?" check below since the system clock may have been | |||||||||||||||||||||||
// moved backward. | |||||||||||||||||||||||
|
|||||||||||||||||||||||
if (s_leapSecondCache is LeapSecondCache cache) | |||||||||||||||||||||||
|
Method | Job | Toolchain | Mean | Error | StdDev | Ratio |
---|---|---|---|---|---|---|
GetUtcNow | Job-LSFSOO | proto | 28.97 ns | 0.112 ns | 0.104 ns | 1.00 |
GetUtcNow | Job-LKFIYO | proto_initcache | 28.85 ns | 0.186 ns | 0.174 ns | 1.00 |
Even if there's no real perf difference, might be worth doing anyway just to simplify things?
Outdated
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 know we talked offline about that and you mentioned the line
ulong deltaTicksFromStartOfMonth = osTicks - cache.OSFileTimeTicksAtStartOfMonth;
will get optimized. but I am not sure why we need to calculate the delta at all every time. we can just get rid of this delta calculation and calculate it once when we are caching it.
When caching, we calculate the delta of ticks = OSTicks - (dotnet calculated ticks from osticks) // let's call it ticksDelta
Also, when caching we'll calculate the OS ticks at the end of the current month at 23:59:59 // let's call it endOfMonthOSTicks
the code will be simplified as follow:
if (s_leapSecondCache is LeapSecondCache cache && cache.endOfMonthOSTicks > osticks)
{
return new DateTime(osticks - cache.ticksDelta + FileTimeOffset); // includes UTC marker
}
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 don't think this optimization would be valid. Consider the case where my system clock is Jan 1, 2017 00h00:01 UTC. I call DateTime.UtcNow
and populate the cache. Then Windows syncs the system clock against the network time server, and it turns out that my machine was oh-so-slightly fast. So I go back 3 seconds, and now my system clock is Dec 31, 2016 23h59:58 UTC per .NET's reckoning. But Dec 31, 2016 had a positive leap second! So 3 seconds behind Jan 1, 2017 00h00:01 UTC should actually be reported as Dec 31, 2016 23h59:59 UTC.
Is there a good way to account for this condition other than including both the start of month and the end of month times in the cache?
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.
looks this scenario will be broken with your code too. no? you can run into the same issue when calculating OSFileTimeTicksAtStartOfMonth too. right? I think if the time on the system can be adjusted at any time we'll break as we'll not feel this adjustment.
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 don't think this scenario is broken with the proposed implementation. If the clock gets set backward to the previous month, then GetSystemTimeAsFileTime
will return a tick count less than our cached value cache.OSFileTimeTicksAtStartOfMonth
, and deltaTicksFromStartOfMonth will end up being a very large positive number (due to integer overflow). This causes the deltaTicksFromStartOfMonth < cache.CacheValidityPeriodInTicks
check to fail, sending us down the fallback code path.
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 understand cannot insert historical leap seconds but you can still insert leap seconds at the end of the list (I guess). meaning you can start caching, then leap second get inserted at the end of previous month (and would be the last leap second in the list for Windows). I think this will break this caching logic. you can use the tool I pointed you at for inserting leap second and then you can try this scenario and look what you'll get.
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.
[Stdcall]
is not necessary. Platform default works fine. We do have explicit StdCall on Windows-specific DllImports in under libraries either.