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

비밀번호

댓글 작성자
 




... 136  137  [138]  139  140  141  142  143  144  145  146  147  148  149  150  ...
NoWriterDateCnt.TitleFile(s)
1605정성태1/23/201421218개발 환경 구성: 212. Visual Studio Online과 "Monaco" 서비스 연동
1604정성태1/23/201421544오류 유형: 216. 윈도우 서버 백업 - Hyper-V 가상 머신이 백업되지 않는 경우 (2)
1603정성태1/23/201433609개발 환경 구성: 211. Hyper-V - Generation 2 유형의 VM 생성 시 ISO 부팅이 안된다면? [1]
1602정성태1/22/201423815디버깅 기술: 62. windbg - 사용자 모드 원격 디버깅
1601정성태1/22/201427428오류 유형: 215. windbg - Symbol file could not be found. Defaulted to export symbols
1600정성태1/19/201424082.NET Framework: 410. C# - 재귀호출을 스택 자료구조와 반복문을 이용해 대체하는 방법을 Paralle.For와 함께? [1]파일 다운로드1
1599정성태1/18/201432193.NET Framework: 409. C# - 재귀호출을 스택 자료구조와 반복문을 이용해 대체하는 방법 [1]파일 다운로드1
1598정성태1/17/201425518디버깅 기술: 61. NT 서비스 시작 단계에서 닷넷 메서드에 BP를 걸어 디버깅하는 방법
1597정성태1/17/201424111Phone: 9. Xamarin Android에 구글 AdMob 사용하는 방법 [1]
1596정성태1/17/201423046오류 유형: 214. Local SYSTEM 계정으로 실행된 IE에서 다운로드가 안 되는 문제
1595정성태1/16/201420043오류 유형: 213. attrib - Not resetting system file
1594정성태1/15/201422190오류 유형: 212. 마이크로소프트 라이브 계정 로그인 실패하는 경우
1593정성태1/14/201420820오류 유형: 211. ASP.NET 응용 프로그램을 IIS Express에서 디버깅할 때 "Requested registry access is not allowed" 오류 발생
1592정성태1/14/201421108오류 유형: 210. 2대의 AD가 있는 경우 도메인에 컴퓨터 추가를 실패한다면? [1]
1591정성태1/14/201423278오류 유형: 209. DebugDiag: Unable to find mscordacwks_x86_x86_[...version...].dll
1590정성태1/14/201423941오류 유형: 208. VSS Writer - NTDS 오류
1589정성태1/14/201432886Windows: 85. 컴퓨터를 껐는데도 어느 순간 자동으로 켜진다면? [2]
1588정성태1/14/201429676Windows: 84. 윈도우 7/8 - 메뉴 항목이 잔상으로 남는 문제
1587정성태1/14/201425580디버깅 기술: 60. NT 서비스가 시작하자마자 디버거를 연결시키는 방법 (2)
1586정성태1/14/201427297디버깅 기술: 59. NT 서비스가 시작하자마자 디버거를 연결시키는 방법 (1) [1]
1585정성태1/14/201430195VS.NET IDE: 84. Visual Studio를 이용한 파일 비교(diff)
1584정성태1/13/201432500Windows: 83. 윈도우 8 - UI가 있는 프로그램을 Local SYSTEM 권한의 세션 0 데스크톱에서 실행하는 방법
1583정성태1/13/201430502Windows: 82. 윈도우 8 - "Interactive Services Detection" 서비스 시작하는 방법 [1]
1582정성태1/12/201428962개발 환경 구성: 210. 원격 데스크톱(RDP) 접속 프로그램 - Royal TS [1]
1581정성태1/12/201430310.NET Framework: 408. 자바와 닷넷의 제네릭 차이점 - 중간 언어 및 공변/반공변 처리 [8]
1580정성태1/12/201440332.NET Framework: 407. 닷넷 사용자 정의 예외 클래스의 최소 구현 코드 [1]
... 136  137  [138]  139  140  141  142  143  144  145  146  147  148  149  150  ...