Microsoft MVP성태의 닷넷 이야기
닷넷: 2309. C# - .NET Core에서 바뀐 DateTime.Ticks의 정밀도 [링크 복사], [링크+제목 복사],
조회: 5511
글쓴 사람
정성태 (seongtaejeong at gmail.com)
홈페이지
첨부 파일
 
(연관된 글이 1개 있습니다.)
(시리즈 글이 8개 있습니다.)
.NET Framework: 614. C# - DateTime.Ticks의 정밀도
; https://www.sysnet.pe.kr/2/0/11082

.NET Framework: 827. C# - 인터넷 시간 서버로부터 받은 시간을 윈도우에 적용하는 방법
; https://www.sysnet.pe.kr/2/0/11883

스크립트: 33. JavaScript와 C#의 시간 변환
; https://www.sysnet.pe.kr/2/0/12849

Windows: 204.  Windows 10부터 바뀐 QueryPerformanceFrequency, QueryPerformanceCounter
; https://www.sysnet.pe.kr/2/0/13035

.NET Framework: 1997. C# - nano 시간을 가져오는 방법
; https://www.sysnet.pe.kr/2/0/13036

스크립트: 47. 파이썬의 time.time() 실숫값을 GoLang / C#에서 사용하는 방법
; https://www.sysnet.pe.kr/2/0/13308

닷넷: 2143. C# - 시스템 Time Zone 변경 시 이벤트 알림을 받는 방법
; https://www.sysnet.pe.kr/2/0/13413

닷넷: 2309. C# - .NET Core에서 바뀐 DateTime.Ticks의 정밀도
; https://www.sysnet.pe.kr/2/0/13803




C# - .NET Core에서 바뀐 DateTime.Ticks의 정밀도

오랜만에 이전 글의 코드를,

C# - DateTime.Ticks의 정밀도
; https://www.sysnet.pe.kr/2/0/11082

using System;

class Program
{
    static void Main(string[] args)
    {
        int count = 100000;
        long[] tickBuf = new long[count];

        for (int i = 0; i < count; i++)
        {
            // tickBuf[i] = DateTime.Now.Ticks;
            tickBuf[i] = DateTime.UtcNow.Ticks; // Now/UtcNow 모두 결과는 동일
        }

        long oldTime = tickBuf[0];
        long elapsed;
        for (int i = 1; i < count; i++)
        {
            elapsed = tickBuf[i] - oldTime;
            oldTime = tickBuf[i];

            if (elapsed != 0)
            {
                Console.WriteLine(elapsed);
            }
        }
    }
}
/* 출력 결과:
...[생략]...
1
1
1
...[생략]...
1
1

.NET 8에서 실행했더니 결과가 예상치 않은 값이 나왔습니다. 즉, 당시 .NET Framework로 실행했을 때는 "Current timer interval"의 변화에 따라, 예를 들어 15.6ms인 경우에는 대략 156,000의 변화가, 1ms인 경우에는 10,000의 변화가 나타났었는데요, .NET 8에서는 1 값이 연속으로 출력됐습니다.

뭔가 분명히 변화가 있다는 것이겠죠? ^^




자, 그럼 UtcNow의 소스코드를 살펴볼까요?

namespace System
{
    public readonly partial struct DateTime
    {
        internal static bool SystemSupportsLeapSeconds => LeapSecondCache.s_systemSupportsLeapSeconds;

        // ...[생략]...

        public static unsafe DateTime UtcNow
        {
            get
            {
                ulong fileTimeTmp; // mark only the temp local as address-taken
                LeapSecondCache.s_pfnGetSystemTimeAsFileTime(&fileTimeTmp);
                ulong fileTime = fileTimeTmp;

                if (LeapSecondCache.s_systemSupportsLeapSeconds)
                {
                    // Query the leap second cache first, which avoids expensive calls to GetFileTimeAsSystemTime.

                    LeapSecondCache cacheValue = LeapSecondCache.s_leapSecondCache;
                    ulong ticksSinceStartOfCacheValidityWindow = fileTime - cacheValue.OSFileTimeTicksAtStartOfValidityWindow;
                    if (ticksSinceStartOfCacheValidityWindow < LeapSecondCache.ValidityPeriodInTicks)
                    {
                        return new DateTime(dateData: cacheValue.DotnetDateDataAtStartOfValidityWindow + ticksSinceStartOfCacheValidityWindow);
                    }

                    return UpdateLeapSecondCacheAndReturnUtcNow(); // couldn't use the cache, go down the slow path
                }
                else
                {
                    return new DateTime(dateData: fileTime + (FileTimeOffset | KindUtc));
                }
            }
        }
    }
}

코드상으로는, 핵심 작업은 LeapSecondCache.s_pfnGetSystemTimeAsFileTime 함수 포인터로 GetSystemTimeAsFileTime 함수를 호출하는 것에서 끝납니다. 이후 윤초 지원 여부를 LeapSecondCache.s_systemSupportsLeapSeconds 속성으로 결정하는데요, 그 2개의 정의를 모두 LeapSecondCache 클래스에서 찾을 수 있습니다.

namespace System
{
    public readonly partial struct DateTime
    {
        internal static bool SystemSupportsLeapSeconds => LeapSecondCache.s_systemSupportsLeapSeconds;

        // ...[생략]...

        private sealed class LeapSecondCache
        {
            // The length of the validity window. Must be less than 23:59:59.
            internal const ulong ValidityPeriodInTicks = TicksPerMinute * 5;

            // The FILETIME value at the beginning of the validity window.
            internal ulong OSFileTimeTicksAtStartOfValidityWindow;

            // The DateTime._dateData value at the beginning of the validity window.
            internal ulong DotnetDateDataAtStartOfValidityWindow;

            // The leap second cache. May be accessed by multiple threads simultaneously.
            // Writers must not mutate the object's fields after the reference is published.
            // Readers are not required to use volatile semantics.
            internal static LeapSecondCache s_leapSecondCache = new LeapSecondCache();

            // The configuration of system leap seconds support is intentionally here to avoid blocking
            // AOT pre-initialization of public readonly DateTime statics.
            internal static readonly bool s_systemSupportsLeapSeconds = GetSystemSupportsLeapSeconds();

            internal static readonly unsafe delegate* unmanaged[SuppressGCTransition]<ulong*, void> 
                s_pfnGetSystemTimeAsFileTime = GetGetSystemTimeAsFileTimeFnPtr();
        }
    }
}

우선 GetSystemSupportsLeapSeconds() 함수로 윤초 지원 여부를 결정하는데요, 이건 Win32 API로 제공하는 NtQuerySystemInformation 함수를 호출해 결정하는 식으로 구현돼 있습니다.

private static unsafe bool GetSystemSupportsLeapSeconds()
{
    Interop.NtDll.SYSTEM_LEAP_SECOND_INFORMATION slsi;

    return Interop.NtDll.NtQuerySystemInformation(
        Interop.NtDll.SystemLeapSecondInformation,
        &slsi,
        (uint)sizeof(Interop.NtDll.SYSTEM_LEAP_SECOND_INFORMATION),
        null) == 0 && slsi.Enabled != Interop.BOOLEAN.FALSE;
}

그러니까, 결국 윤초 지원에 따른 결과로는 interrupt timer 주기를 따르지 않는 이유가 되지 못합니다.

자, 그럼 이제 남은 문제는 s_pfnGetSystemTimeAsFileTime 함수 포인터인데요, 이것을 결정하는 GetGetSystemTimeAsFileTimeFnPtr 함수를 보면,

private static unsafe delegate* unmanaged[SuppressGCTransition]<ulong*, void> GetGetSystemTimeAsFileTimeFnPtr()
{
    IntPtr kernel32Lib = Interop.Kernel32.LoadLibraryEx(Interop.Libraries.Kernel32, IntPtr.Zero, Interop.Kernel32.LOAD_LIBRARY_SEARCH_SYSTEM32);
    Debug.Assert(kernel32Lib != IntPtr.Zero);

    IntPtr pfnGetSystemTime = NativeLibrary.GetExport(kernel32Lib, "GetSystemTimeAsFileTime");

    if (NativeLibrary.TryGetExport(kernel32Lib, "GetSystemTimePreciseAsFileTime", out IntPtr pfnGetSystemTimePrecise))
    {
        // GetSystemTimePreciseAsFileTime exists and 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.

        // Retry this check several times to reduce chance of false negatives due to thread being rescheduled
        // at wrong time.
        for (int i = 0; i < 10; i++)
        {
            long systemTimeResult, preciseSystemTimeResult;
            ((delegate* unmanaged[SuppressGCTransition]<long*, void>)pfnGetSystemTime)(&systemTimeResult);
            ((delegate* unmanaged[SuppressGCTransition]<long*, void>)pfnGetSystemTimePrecise)(&preciseSystemTimeResult);

            if (Math.Abs(preciseSystemTimeResult - systemTimeResult) <= 100 * TicksPerMillisecond)
            {
                pfnGetSystemTime = pfnGetSystemTimePrecise; // use the precise version
                break;
            }
        }
    }

    return (delegate* unmanaged[SuppressGCTransition]<ulong*, void>)pfnGetSystemTime;
}

아하~~~ 저렇게 되어 있었군요. ^^ 우선 GetSystemTimeAsFileTime 함수를 호출한 다음, (Windows 8/Server 2012 이후부터 지원하는) GetSystemTimePreciseAsFileTime 함수가 있다면 (9014 이슈에 걸리지 않는 경우) 그것을 사용하기로 돼 있는 것입니다.

결국, .NET Core부터는 GetSystemTimePreciseAsFileTime 함수로 호출이 바뀐 탓에,

GetSystemTimeAsFileTime과 GetSystemTimePreciseAsFileTime의 차이점
; https://www.sysnet.pe.kr/2/0/13802

.NET Framework의 실행 결과와 차이가 발생한 것입니다.




참고로, 지난 글에 설명한 QueryPerformanceFrequency 함수의 경우 닷넷에서는 Stopwatch.Frequency 필드로 구할 수 있습니다.

또한, Stopwatch.IsHighResolution 필드는 C++에서는 QueryPerformanceFrequency 함수의 반환값에 해당합니다.

마지막으로, 윤초 지원이 아닌 경우 곧바로 DateTime에 FileTimeOffset과 KindUtc를 더하는데요,

if (LeapSecondCache.s_systemSupportsLeapSeconds)
{
    // ...[생략]...
}
else
{
    return new DateTime(dateData: fileTime + (FileTimeOffset | KindUtc));
}

우선 KindUtc는 "private const ulong KindUtc = 0x4000000000000000;"에 해당하는 상수이고, FileTimeOffset 역시 내부적으로 윤초가 지원되지 않는 경우 고정으로 계산된 상수입니다.

// 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
// 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

// Number of days from 1/1/0001 to 12/31/1600
private const int DaysTo1601 = DaysPer400Years * 4;          // 584388

private const long FileTimeOffset = DaysTo1601 * TicksPerDay;

// Number of 100ns ticks per time unit
internal const int MicrosecondsPerMillisecond = 1000;
private const long TicksPerMicrosecond = 10;
private const long TicksPerMillisecond = TicksPerMicrosecond * MicrosecondsPerMillisecond;

private const int HoursPerDay = 24;
private const long TicksPerSecond = TicksPerMillisecond * 1000;
private const long TicksPerMinute = TicksPerSecond * 60;
private const long TicksPerHour = TicksPerMinute * 60;
private const long TicksPerDay = TicksPerHour * HoursPerDay;




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 11/7/2024]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 91  92  93  94  95  96  97  98  99  100  101  102  103  104  [105]  ...
NoWriterDateCnt.TitleFile(s)
11300정성태9/10/201721160.NET Framework: 681. dotnet.exe - run, exec, build, restore, publish 차이점 [3]
11299정성태9/9/201719830개발 환경 구성: 330. Hyper-V VM의 Internal Network를 Private 유형으로 만드는 방법
11298정성태9/8/201723151VC++: 119. EnumProcesses / EnumProcessModules API 사용 시 주의점 [1]
11297정성태9/8/201719814디버깅 기술: 96. windbg - 풀 덤프에 포함된 모든 닷넷 모듈을 파일로 저장하는 방법
11296정성태9/8/201722934웹: 36. Edge - "이 웹 사이트는 이전 기술에서 실행되며 Internet Explorer에서만 작동합니다." 끄는 방법
11295정성태9/7/201720423디버깅 기술: 95. Windbg - .foreach 사용법
11294정성태9/4/201720090개발 환경 구성: 329. 마이크로소프트의 CoreCLR 프로파일러 예제 빌드 방법 [1]
11293정성태9/4/201720687개발 환경 구성: 328. Visual Studio(devenv.exe)를 배치 파일(.bat)을 통해 실행하는 방법
11292정성태9/4/201718885오류 유형: 419. Cannot connect to WMI provider - Invalid class [0x80041010]
11291정성태9/3/201720712개발 환경 구성: 327. 아파치 서버 2.4를 위한 mod_aspdotnet 마이그레이션
11290정성태9/3/201723970개발 환경 구성: 326. 아파치 서버에서 ASP.NET을 실행하는 mod_aspdotnet 모듈 [2]
11289정성태9/3/201721623개발 환경 구성: 325. GAC에 어셈블리 등록을 위해 gacutil.exe을 사용하는 경우 주의 사항
11288정성태9/3/201718429개발 환경 구성: 324. 윈도우용 XAMPP의 아파치 서버 구성 방법
11287정성태9/1/201727595.NET Framework: 680. C# - 작업자(Worker) 스레드와 UI 스레드 [11]
11286정성태8/28/201714997기타: 67. App Privacy Policy
11285정성태8/28/201723513.NET Framework: 679. C# - 개인 키 보안의 SFTP를 이용한 파일 업로드파일 다운로드1
11284정성태8/27/201721535.NET Framework: 678. 데스크톱 윈도우 응용 프로그램에서 UWP 라이브러리를 이용한 비디오 장치 열람하는 방법 [1]파일 다운로드1
11283정성태8/27/201717313오류 유형: 418. CSS3117: @font-face failed cross-origin request. Resource access is restricted.
11282정성태8/26/201719755Math: 22. 행렬로 바라보는 피보나치 수열
11281정성태8/26/201721587.NET Framework: 677. Visual Studio 2017 - NuGet 패키지를 직접 참조하는 PackageReference 지원 [2]
11280정성태8/24/201718587디버깅 기술: 94. windbg - 풀 덤프에 포함된 모든 모듈을 파일로 저장하는 방법
11279정성태8/23/201730183.NET Framework: 676. C# Thread가 Running 상태인지 아는 방법
11278정성태8/23/201718417오류 유형: 417. TFS - Warning - Unable to refresh ... because you have a pending edit. [1]
11277정성태8/23/201719663오류 유형: 416. msbuild - error MSB4062: The "TransformXml" task could not be loaded from the assembly
11276정성태8/23/201723921.NET Framework: 675. C# - (파일) 확장자와 연결된 실행 파일 경로 찾기 [2]파일 다운로드1
11275정성태8/23/201732943개발 환경 구성: 323. Visual Studio 설치 없이 빌드 환경 구성 - Visual Studio 2017용 Build Tools [1]
... 91  92  93  94  95  96  97  98  99  100  101  102  103  104  [105]  ...