Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Closed
Changes from 1 commit
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
Next Next commit
Improve performance of DateTime.UtcNow
  • Loading branch information
GrabYourPitchforks committed Nov 14, 2020
commit 3e0d82d561daae09e7303b6bf3c9deda31ba1c9b
123 changes: 117 additions & 6 deletions src/libraries/System.Private.CoreLib/src/System/DateTime.Windows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
private static unsafe delegate* unmanaged[Stdcall]<ulong*, void> s_pfnGetSystemTimeAsFileTime = GetGetSystemTimeAsFileTimeFnPtr();
private static unsafe delegate* unmanaged<ulong*, void> s_pfnGetSystemTimeAsFileTime = GetGetSystemTimeAsFileTimeFnPtr();

[Stdcall] is not necessary. Platform default works fine. We do have explicit StdCall on Windows-specific DllImports in under libraries either.

Copy link
Member

Choose a reason for hiding this comment

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

Exercise edge cases manually inside a debugger

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.

Copy link
Member Author

Choose a reason for hiding this comment

The 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 DateTime.UtcNow, and if that code is running in another thread while our unit test is operating it could cause some weird side effects. Would need to brainstorm a bit to see if there's a refactoring that might make this less fragile.

Copy link
Member

@tarekgh tarekgh Nov 17, 2020

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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 readonly then JIT can potentially inline it as a constant?

Copy link
Member

Choose a reason for hiding this comment

The 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.
Copy link
Member

Choose a reason for hiding this comment

The 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);
Copy link
Contributor

Choose a reason for hiding this comment

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

Could write as osTicks + (FileTimeOffset | KindUtc) to enable constant folding.

}

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)
Copy link
Member

Choose a reason for hiding this comment

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

Do we need to do this check every time too? can we just initialize instance with some value? or you have it for optimization too?

Copy link
Member Author

Choose a reason for hiding this comment

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

Interesting idea. We could initialize it with a dummy value like (0, 0, 0). It would save two instructions (a comparison + conditional branch) in the x64 codegen. Benchmark doesn't really show a difference since the branch predictor is primed to assume the field is initialized.

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?

{
if (osTicks >= cache.WindowsTicksAsOfMidnight && osTicks < (cache.WindowsTicksAsOfMidnight + MinTicksPerDay))
{
Copy link
Member

@tarekgh tarekgh Nov 17, 2020

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
        }

Copy link
Member Author

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?

Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

@tarekgh tarekgh Nov 17, 2020

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.

return new DateTime((osTicks - cache.WindowsTicksAsOfMidnight + cache.DateTimeTicksAsOfMidnight) | KindUtc);
}
}

// If we reached this point, one of the following is true:
// a) the cache hasn't yet been initialized; or
// b) the day has changed since the last call to UtcNow; or
// c) the current time is 23:59:59 or 23:59:60.
//
// In cases (a) and (b), we'll update the cache. In case (c), we
// pessimistically assume we might be inside a leap second, so we
// won't update the cache.

DateTime dateTime = FromFileTimeLeapSecondsAware((long)osTicks);
ulong ticksIntoDay = (ulong)dateTime.Ticks % (ulong)TicksPerDay;
if (ticksIntoDay < MinTicksPerDay)
{
// It's not yet 23:59:59, so update the cache. It's ok for multiple
// threads to do this concurrently as long as the write to the static
// is published *after* the cache object's fields have been populated.

Volatile.Write(ref s_leapSecondCache, new LeapSecondCache
{
WindowsTicksAsOfMidnight = osTicks - ticksIntoDay,
DateTimeTicksAsOfMidnight = (ulong)dateTime.Ticks - ticksIntoDay
});
}

return dateTime;
}
}

private sealed class LeapSecondCache
{
internal ulong WindowsTicksAsOfMidnight;
internal ulong DateTimeTicksAsOfMidnight;
}

internal static readonly bool s_systemSupportsLeapSeconds = SystemSupportsLeapSeconds();

internal static unsafe bool IsValidTimeWithLeapSeconds(int year, int month, int day, int hour, int minute, int second, DateTimeKind kind)
{
DateTime dt = new DateTime(year, month, day);
Expand Down