Microsoft MVP성태의 닷넷 이야기
닷넷: 2309. C# - .NET Core에서 바뀐 DateTime.Ticks의 정밀도 [링크 복사], [링크+제목 복사],
조회: 5466
글쓴 사람
정성태 (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

비밀번호

댓글 작성자
 




... 76  77  78  79  80  81  82  83  [84]  85  86  87  88  89  90  ...
NoWriterDateCnt.TitleFile(s)
11837정성태3/6/201939810기타: 74. 도서: 시작하세요! C# 7.3 프로그래밍 [10]
11836정성태3/5/201923382오류 유형: 525. Visual Studio 2019 Preview 4/RC - C# 8.0 Missing compiler required member 'System.Range..ctor' [1]
11835정성태3/5/201921862.NET Framework: 810. C# 8.0의 Index/Range 연산자를 .NET Framework에서 사용하는 방법 및 비동기 스트림의 컴파일 방법 [3]파일 다운로드1
11834정성태3/4/201920653개발 환경 구성: 432. Visual Studio 없이 최신 C# (8.0) 컴파일러를 사용하는 방법
11833정성태3/4/201921205개발 환경 구성: 431. Visual Studio 2019 - CMake를 이용한 공유/실행(so/out) 리눅스 프로젝트 설정파일 다운로드1
11832정성태3/4/201917119오류 유형: 524. Visual Studio CMake - rsync: connection unexpectedly closed
11831정성태3/4/201916950오류 유형: 523. Visual Studio 2019 - 새 창으로 뜬 윈도우를 닫을 때 비정상 종료
11830정성태2/26/201916637오류 유형: 522. 이벤트 로그 - Error opening event log file State. Log will not be processed. Return code from OpenEventLog is 87.
11829정성태2/26/201918327개발 환경 구성: 430. 마이크로소프트의 CoreCLR 프로파일러 예제 빌드 방법 - 리눅스 환경 [1]
11828정성태2/26/201926240개발 환경 구성: 429. Component Services 관리자의 RuntimeBroker 설정이 2개 있는 경우 [8]
11827정성태2/26/201919178오류 유형: 521. Visual Studio - Could not start the 'rsync' command on the remote host, please install it using your system package manager.
11826정성태2/26/201919355오류 유형: 520. 우분투에 .NET Core SDK 설치 시 패키지 의존성 오류
11825정성태2/25/201924630개발 환경 구성: 428. Visual Studio 2019 - CMake를 이용한 리눅스 빌드 환경 설정 [1]
11824정성태2/25/201919060오류 유형: 519. The SNMP Service encountered an error while accessing the registry key SYSTEM\CurrentControlSet\Services\SNMP\Parameters\TrapConfiguration. [1]
11823정성태2/21/201920653오류 유형: 518. IIS 관리 콘솔이 뜨지 않는 문제
11822정성태2/20/201919023오류 유형: 517. docker에 설치한 MongoDB 서버로 연결이 안 되는 경우
11821정성태2/20/201919765오류 유형: 516. Visual Studio 2019 - This extension uses deprecated APIs and is at risk of not functioning in a future VS update. [1]
11820정성태2/20/201922831오류 유형: 515. 윈도우 10 1809 업데이트 후 "User Profiles Service" 1534 경고 발생
11819정성태2/20/201922099Windows: 158. 컴퓨터와 사용자의 SID(security identifier) 확인 방법
11818정성태2/20/201920187VS.NET IDE: 131. Visual Studio 2019 Preview의 닷넷 프로젝트 빌드가 20초 이상 걸리는 경우 [2]
11817정성태2/17/201916533오류 유형: 514. WinDbg Preview 실행 오류 - Error : DbgX.dll : WindowsDebugger.WindowsDebuggerException: Could not load dbgeng.dll
11816정성태2/17/201920003Windows: 157. 윈도우 스토어 앱(Microsoft Store App)을 명령행에서 직접 실행하는 방법
11815정성태2/14/201918242오류 유형: 513. Visual Studio 2019 - VSIX 설치 시 "The extension cannot be installed to this product due to prerequisites that cannot be resolved." 오류 발생
11814정성태2/12/201917073오류 유형: 512. VM(가상 머신)의 NT 서비스들이 자동 시작되지 않는 문제
11813정성태2/12/201918389.NET Framework: 809. C# - ("Save File Dialog" 등의) 대화 창에 확장 속성을 보이는 방법
11812정성태2/11/201915704오류 유형: 511. Windows Server 2003 VM 부팅 후 로그인 시점에 0xC0000005 BSOD 발생
... 76  77  78  79  80  81  82  83  [84]  85  86  87  88  89  90  ...