Microsoft MVP성태의 닷넷 이야기
닷넷: 2309. C# - .NET Core에서 바뀐 DateTime.Ticks의 정밀도 [링크 복사], [링크+제목 복사],
조회: 6883
글쓴 사람
정성태 (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)
11846정성태3/15/201923166Linux: 6. getenv, setenv가 언어/운영체제마다 호환이 안 되는 문제
11845정성태3/15/201922849Linux: 5. Linux 응용 프로그램의 (C++) so 의존성 줄이기(ReleaseMinDependency) [3]
11844정성태3/14/201924597개발 환경 구성: 434. Visual Studio 2019 - 리눅스 프로젝트를 이용한 공유/실행(so/out) 프로그램 개발 환경 설정 [1]파일 다운로드1
11843정성태3/14/201919416기타: 75. MSDN 웹 사이트를 기본으로 영문 페이지로 열고 싶다면?
11842정성태3/13/201917285개발 환경 구성: 433. 마이크로소프트의 CoreCLR 프로파일러 예제를 Visual Studio CMake로 빌드하는 방법 [1]파일 다운로드1
11841정성태3/13/201917771VS.NET IDE: 132. Visual Studio 2019 - CMake의 컴파일러를 기본 g++에서 clang++로 변경
11840정성태3/13/201919840오류 유형: 526. 윈도우 10 Ubuntu App 환경에서는 USB 외장 하드 접근 불가
11839정성태3/12/201923905디버깅 기술: 124. .NET Core 웹 앱을 호스팅하는 Azure App Services의 프로세스 메모리 덤프 및 windbg 분석 개요 [3]
11838정성태3/7/201927636.NET Framework: 811. (번역글) .NET Internals Cookbook Part 1 - Exceptions, filters and corrupted processes [1]파일 다운로드1
11837정성태3/6/201941290기타: 74. 도서: 시작하세요! C# 7.3 프로그래밍 [10]
11836정성태3/5/201924934오류 유형: 525. Visual Studio 2019 Preview 4/RC - C# 8.0 Missing compiler required member 'System.Range..ctor' [1]
11835정성태3/5/201923079.NET Framework: 810. C# 8.0의 Index/Range 연산자를 .NET Framework에서 사용하는 방법 및 비동기 스트림의 컴파일 방법 [3]파일 다운로드1
11834정성태3/4/201921799개발 환경 구성: 432. Visual Studio 없이 최신 C# (8.0) 컴파일러를 사용하는 방법
11833정성태3/4/201922831개발 환경 구성: 431. Visual Studio 2019 - CMake를 이용한 공유/실행(so/out) 리눅스 프로젝트 설정파일 다운로드1
11832정성태3/4/201917841오류 유형: 524. Visual Studio CMake - rsync: connection unexpectedly closed
11831정성태3/4/201918586오류 유형: 523. Visual Studio 2019 - 새 창으로 뜬 윈도우를 닫을 때 비정상 종료
11830정성태2/26/201917833오류 유형: 522. 이벤트 로그 - Error opening event log file State. Log will not be processed. Return code from OpenEventLog is 87.
11829정성태2/26/201919101개발 환경 구성: 430. 마이크로소프트의 CoreCLR 프로파일러 예제 빌드 방법 - 리눅스 환경 [1]
11828정성태2/26/201927619개발 환경 구성: 429. Component Services 관리자의 RuntimeBroker 설정이 2개 있는 경우 [8]
11827정성태2/26/201920054오류 유형: 521. Visual Studio - Could not start the 'rsync' command on the remote host, please install it using your system package manager.
11826정성태2/26/201920604오류 유형: 520. 우분투에 .NET Core SDK 설치 시 패키지 의존성 오류
11825정성태2/25/201926253개발 환경 구성: 428. Visual Studio 2019 - CMake를 이용한 리눅스 빌드 환경 설정 [1]
11824정성태2/25/201920507오류 유형: 519. The SNMP Service encountered an error while accessing the registry key SYSTEM\CurrentControlSet\Services\SNMP\Parameters\TrapConfiguration. [1]
11823정성태2/21/201921452오류 유형: 518. IIS 관리 콘솔이 뜨지 않는 문제
11822정성태2/20/201920659오류 유형: 517. docker에 설치한 MongoDB 서버로 연결이 안 되는 경우
11821정성태2/20/201921429오류 유형: 516. Visual Studio 2019 - This extension uses deprecated APIs and is at risk of not functioning in a future VS update. [1]
... 76  77  78  79  80  81  82  83  84  [85]  86  87  88  89  90  ...