Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
[qpc.zip]    
(연관된 글이 4개 있습니다.)
(시리즈 글이 5개 있습니다.)
Windows: 120. 윈도우 운영체제의 시간 함수 (1) - GetTickCount와 timeGetTime의 차이점
; https://www.sysnet.pe.kr/2/0/11063

Windows: 121. 윈도우 운영체제의 시간 함수 (2) - Sleep 함수의 동작 방식
; https://www.sysnet.pe.kr/2/0/11065

Windows: 122. 윈도우 운영체제의 시간 함수 (3) - QueryInterruptTimePrecise, QueryInterruptTime 함수
; https://www.sysnet.pe.kr/2/0/11066

Windows: 123. 윈도우 운영체제의 시간 함수 (4) - RTC, TSC, PM Clock, HPET Timer
; https://www.sysnet.pe.kr/2/0/11067

Windows: 124. 윈도우 운영체제의 시간 함수 (5) - TSC(Time Stamp Counter)와 QueryPerformanceCounter
; https://www.sysnet.pe.kr/2/0/11068




윈도우 운영체제의 시간 함수 (5) - TSC(Time Stamp Counter)와 QueryPerformanceCounter

지난 글에서 말로만 설명한 TSC를 코딩으로도 실습해 보겠습니다. ^^

TSC는 CPU 차원에서 제공되는 카운터로 CPU가 리셋된 이후 동작한 CPU 사이클의 수를 "Time Stamp Counter"라는 64비트 레지스터에 보관하고 rdtsc/rdtscp 기계어로 접근할 수 있습니다.

Visual C++의 경우 이를 위해 __rdtsc 내부 함수를 제공합니다.

__rdtsc (ReaD Time Stamp Counter)
; https://learn.microsoft.com/en-us/cpp/intrinsics/rdtsc

아래는 간단한 사용법입니다.

#include "stdafx.h"
#include <Windows.h>

int main()
{
    int count = 10;

    while (count -- > 0)
    {
        printf("%I64d\n", ::__rdtsc());
    }

    return 0;
}

별로 의미가 없지만 출력 결과는 이렇습니다.

173595388146208
173595388675124
173595388799672
173595389066244
173595389245210
173595389459434
173595389709418
...[생략]...

__rdtsc 외에도 ReadTimeStampCounter 함수를 제공하는데 이는 단순한 매크로 함수에 불과합니다.

#define ReadTimeStampCounter() __rdtsc()




출력 결과에서 본 것처럼, "CPU 사이클의 수"이기 때문에 여러분들의 CPU가 몇 GHz냐에 따라 이 수치를 밀리초 등의 단위로 바꾸는 방법이 달라지게 됩니다.

일단, 정말로 TSC가 "CPU 사이클의 수"인지 다음과 같은 코드로 대략 확인할 수 있습니다.

#include "stdafx.h"
#include <Windows.h>
#include <ctime>

#pragma comment(lib, "winmm.lib")

int main()
{
    int count = 100;

    timeBeginPeriod(1); // timeGetTime의 정밀도를 1ms로 높이고,
    const DWORD msDuration = 1000;
    const int iterations = 6;

    for (int i = 0; i < iterations; ++i)
    {
        __int64 rdtscStart = __rdtsc(); // 현재의 cpu cycle 수를 구하고,
        
        DWORD startTick = timeGetTime();
        for (;;)                        // 약 1초 동안 지연,
        {
            DWORD tickDuration = timeGetTime() - startTick;
            if (tickDuration >= msDuration)
            {
                break;
            }
        }

        __int64 rdtscElapsed = __rdtsc() - rdtscStart; // 1초가 지난 후 cpu cycle 수를 구함

        printf("  elapsed: %I64d, %0.4fGHz\n", rdtscElapsed, rdtscElapsed / 1000.0 / 1000 / 1000);
    }

    timeEndPeriod(1);

    return 0;
}

제 컴퓨터에서 출력 결과는 다음과 같습니다.

  elapsed: 3408820574, 3.4088GHz
  elapsed: 3404523320, 3.4045GHz
  elapsed: 3408260224, 3.4083GHz
  elapsed: 3407562582, 3.4076GHz
  elapsed: 3407761180, 3.4078GHz
  elapsed: 3406738008, 3.4067GHz

실제로 제 컴퓨터는 3.4GHz의 i5 CPU인데 대략 비슷한 값을 보이고 있습니다.




이전 글에서도 언급했지만, TSC 레지스터가 cpu cycle의 수를 담고 있기 때문에 이후 Turbo-Boost 기능이 추가된 CPU의 경우 이것이 문제가 됩니다. 실행 중에 동적으로 CPU 사이클이 3.4GHz에서 3.8GHz 등으로 변화할 수 있기 때문에 특정 함수의 성능을 측정하기 위해 사용한 __rdtsc의 반환 값을 어떻게 해석해야 할지 난감하게 된 것입니다.

이런 문제를 다행히 CPU 제조사에서 해결을 해주었는데, 동적으로 변화하는 CPU Cycle 수를 TSC 레지스터에 반영하지 않고 CPU SPEC에 명시된 (제 경우 3.4GHz) 사이클 기준으로 CPU 내부에서 적절하게 변환해 주는 기능을 추가한 것입니다. 이름하여 "Invariant TSC"라고 합니다.

실제로 sysinternals의 coreinfo.exe를 이용해 자신의 CPU에 이런 기능이 적용되었는지 쉽게 확인할 수 있습니다.

C:\>coreinfo

Coreinfo v3.31 - Dump information on system CPU and memory topology
Copyright (C) 2008-2014 Mark Russinovich
Sysinternals - www.sysinternals.com

Intel(R) Core(TM) i7-6700 CPU @ 3.40GHz
Intel64 Family 6 Model 94 Stepping 3, GenuineIntel
Microcode signature: 00000023
...[생략]...
INVPCID         -       Supports INVPCID instruction
PDCM            -       Supports Performance Capabilities MSR
RDTSCP          *       Supports RDTSCP instruction
TSC             *       Supports RDTSC instruction
TSC-DEADLINE    -       Local APIC supports one-shot deadline timer
TSC-INVARIANT   *       TSC runs at constant rate
xTPR            *       Supports disabling task priority messages
...[생략]...

다음의 글은, "Invariant TSC" 기능이 있는 환경에서 "Turbo Boost" On/Off에 상관없이 TSC의 값이 일관성 있게 나온다는 것을 테스트하고 있습니다.

rdtsc in the Age of Sandybridge
; https://randomascii.wordpress.com/2011/07/29/rdtsc-in-the-age-of-sandybridge/

추가로 coreinfo.exe의 출력 결과 중에 "RDTSCP" 항목을 잠깐 설명해 보겠습니다. 지난 글에서 언급한 것처럼, 명령어의 실행 순서가 보장받지 못하는 환경에서도 rdtsc를 사용하면 값의 왜곡이 발생한다고 했는데요. 이런 경우의 대안으로 직렬화가 수행되는 명령어를 넣어주면 되는데 대표적인 예로 cpuid가 있어서 다음과 같이 한다고 합니다.

unsigned __int64 inline GetRDTSC() {
   __asm {
      ; Flush the pipeline
      XOR eax, eax
      CPUID
      ; Get RDTSC counter in edx:eax
      RDTSC
   }
}

CPU 제조사에서는 이를 하나로 묶는 명령어를 이후 추가하게 되었는데 그것이 바로 rdtscp입니다.

참고로, TSC 레지스터를 읽어들이는 것은 Ring 0 수준에서만 가능하도록 바꿀 수도 있습니다. 물론, Ring 3 수준에서 읽는 것을 막고 싶다면 그것을 수행하는 작업을 대행하는 device driver를 만들면 됩니다.

어쨌든, 대충 살펴보는 것만으로 TSC를 직접 사용하려면 환경에 따라 고려해야 할 것이 많다는 사실이 별로 탐탁지 않으실 것입니다. ^^




이런저런 문제로 인해, 마이크로소프트는 TSC를 직접 사용하는 것을 지양하고 대신 TSC의 단점들을 운영체제 차원에서 보완한 QueryPerformanceCounter를 사용할 것을 권장하고 있으며 문서상으로도 QPC가 다중 코어/프로세서 및 하이퍼스레딩에서조차도 신뢰할 수 있는 값을 제공한다고 명시하고 있습니다.

QueryPerformanceCounter는 TSC와 개념이 비슷하므로 마찬가지로 사용 방법도 간단합니다.

#include "stdafx.h"
#include <Windows.h>
#include <ctime>

#pragma comment(lib, "winmm.lib")

__int64 GetQPCTime()
{
    LARGE_INTEGER qpcTime;
    QueryPerformanceCounter(&qpcTime);

    __int64 t = __rdtsc();

    return qpcTime.QuadPart;
}

__int64 GetQPCFreq()
{
    LARGE_INTEGER qpcRate;
    QueryPerformanceFrequency(&qpcRate);
    return qpcRate.QuadPart;
}

int main()
{
    timeBeginPeriod(1);

    const DWORD msDuration = 1000;
    const int iterations = 6;
    const __int64 qpcFreqency = GetQPCFreq();

    for (int i = 0; i < iterations; ++i)
    {
        __int64 qpcStart = GetQPCTime();
        
        DWORD startTick = timeGetTime();
        for (;;)
        {
            DWORD tickDuration = timeGetTime() - startTick;
            if (tickDuration >= msDuration)
            {
                break;
            }
        }

        __int64 qpcElapsed = GetQPCTime() - qpcStart;
        printf("%I64d / %I64d == %0.4f\n", qpcElapsed, qpcFreqency, ((double)qpcElapsed / qpcFreqency));
    }

    timeEndPeriod(1);

    return 0;
}

(업데이트 2022-04-22: 위의 소스 코드는 이제 다르게 동작합니다. 보다 자세한 사항은 "Windows 10부터 바뀐 QueryPerformanceFrequency, QueryPerformanceCounter" 글에서 다루므로 참고하세요.)

위의 코드를 수행한 결과는 이렇습니다.

3327949 / 3328129 == 0.9999
3327319 / 3328129 == 0.9998
3329828 / 3328129 == 1.0005
3325995 / 3328129 == 0.9994
3329377 / 3328129 == 1.0004
3325932 / 3328129 == 0.9993

거의 1초에 가까운 지연 시간을 확인할 수 있습니다.

QueryPerformanceFrequency가 반환한 것은 초당 tick 수입니다. 따라서 만약 이 값이 (위의 출력 결과에서는 3,328,129) 3,125,000이라면, 주기 값 T = 1 / 3,125,000 = 0.000000320(320 nanoseconds)가 됩니다. 즉, QueryPerformanceCounter 값의 차이가 1이라면 (이론상) 320나노 초가 지난 시간임을 의미합니다. 당연하겠지만, 이런 시스템에서는 시간값의 정밀도를 320나노 초 이하로 내리는 것은 불가능합니다.




알아본 김에 마지막으로 C++의 clock 함수를 설명해 보겠습니다.

clock
; https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/clock

이 함수 역시 내부적으로는 (윈도우 환경에서라면) QueryPerformanceCounter를 사용하고, 따라서 (대부분의 경우) TSC를 사용하는 것과 다름없습니다. 실제로 Windows SDK가 설치된 경우 다음과 같은 경로를 통해 소스 코드를 보시면 확인할 수 있습니다.

// C:\Program Files (x86)\Windows Kits\10\Source\10.0.10240.0\ucrt\time\clock.cpp

extern "C" clock_t __cdecl clock()
{
    if (start_count == -1)
        return -1;

    LARGE_INTEGER current_count;
    if (!QueryPerformanceCounter(&current_count))
        return -1;

    long long const result = current_count.QuadPart - start_count;
    if (result < 0)
        return -1;

    long long const scaled_result = scale_count(result);

    // Per C11 7.27.2.1 ("The clock function")/3, "If the processor time used...
    // cannot be represented, the function returns the value (clock_t)(-1)."
    if (scaled_result > LONG_MAX)
        return -1;

    return static_cast<clock_t>(scaled_result);
}

extern "C" int __cdecl __acrt_initialize_clock()
{
    LARGE_INTEGER local_frequency;
    LARGE_INTEGER local_start_count;
    if (!QueryPerformanceFrequency(&local_frequency) ||
        !QueryPerformanceCounter(&local_start_count) ||
        local_frequency.QuadPart == 0)
    {
        source_frequency = -1; 
        start_count      = -1;
        return 0;
    }

    source_frequency = local_frequency.QuadPart;
    start_count      = local_start_count.QuadPart;

    return 0;
}

단지, clock 함수는 1초 당 1000 clocks가 되는 것을 기준으로 QueryPerformanceCounter의 단위를 좀 더 다듬었다고 보시면 됩니다.

// time.h
#define CLOCKS_PER_SEC  ((clock_t)1000)

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




참고로, 이번 5부에 걸친 내용은 시스템 하위의 것을 포함하고 있기 때문에 언제든지 deprecated 될 수 있습니다.

실제로, 웹상에 있는 많은 자료들이 초기 TSC 구현의 것을 모델로 설명한 것들이 많아서 이제는 진실이 아닌 것이 되어버렸는데, (일례로 QueryPerformanceCounter는 스레드 친화도를 설정해야 정확한 값이 보장된다는 식) 이 글 역시 시간 앞에서 자유로울 수 없습니다. (음... 그러니까 미리... ^^ 쉴드를 쳐봅니다.)

마지막으로, 정리를 해보면.

  • GetTickCount/GetTickCount64는 대충 20ms 정도의 별로 중요하지 않은 시간 간격을 측정하는 용도라면 좋습니다.
  • timeGetTime은 1ms 단위의 시간 측정에 좋습니다. 대신 목적에 따라 timeBeginPeriod, timeEndPeriod를 이용해 정밀도를 조정해야 할 필요가 있습니다. (아울러 조정된 정밀도만큼, 전력 소비가 늘어난다는 단점)
  • TSC는 높은 신뢰성을 요구하지 않는 환경에서는 정밀함과 속도를 만족합니다.
  • QueryPerformanceCounter는 TSC의 단점을 보완하지만, 구형 컴퓨터에 설치된 Windows XP를 고려해야 한다면 timeGetTime을 사용합니다.




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 11/14/2023]

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

비밀번호

댓글 작성자
 



2018-06-11 01시51분
[질문드립니닷] 안녕하세요, 또 한번 질문드릴게 있습니다.. ㅎㅎ..
저번에 말씀주신대로 쓰레드를 구성하고 우선순위를 최고 우선순위보다 높게 지정 한 후에
올려주신 소스의 구성도 살짝 바꿔보았는데요, 올려주신대로 start와 현재의 timeGetTime() 차가 1일때가 아닌
현재 QPC 값에서 start했을때의 QPC가 QueryPerformanceFrequency/1000 의 값보다 커졌을때로 바꿨습니다.
그런데 결과를 보면 동일하게 클럭값의 차를 통해 계산했을때 꾸준히(어쩌면 당연하게..?) 1ms 이지만
그때의 timeGetTime 값의 차를 비교해보면 0 혹은 1이 꾸준히 나오다가 아주 가끔 15 정도의 튀는 값이 있더군요.
이 방식이 유효한 시간 측정 방식이라고 볼수 있는 것인지에 대해서 질문드립니닷.
[guest]
2018-06-11 04시42분
질문을 정리해 보면, QueryPerformanceFrequency/1000로 할 때는 항상 1ms로 나오지만 측정 코드를 timeGetTime으로 했을 때는 가끔씩 15가 나온다는 건가요?
정성태
2018-06-11 05시24분
[질문드립니닷] 아 제가 깔끔하게 적지를 못했군요. QPF/1000과 timeGetTime을 함께 진행해 보았습니다.
일단 QPF 이용한 것으로 start 지점과 QPF/1000 보다 커진 지점 사이의 시간이 항상 정확히 1ms는 아니지만
허용 오차범위 이내(0.05ms)에서 시간 측정이 잘 되고있습니다. (약 0.98@@~1.05@@ 정도 나옵니다.)
또한 함께 start 지점과 QPF/1000 보다 커진 지점 사이의 timeGetTime 차를 찍어보면
기준이 1ms이기에 예상대로라면 값이 1 혹은 넓게 잡아도 0~2 의 값이 나올 것으로 예상했으나
아주 가끔씩 (코드 수행을 여러번 해보다보면) 15 같은 값이 나옵니다.
일단 간단한 clock 함수 이용해서 코드 수행시간을 타이머 코드 위 아래로 놓고 iteration 기준으로 측정하여보면 1ms 단위로 측정되는 것이 맞는것 같으며
값이 튈때에도 시간이 오차 범위 이내로 나오기 때문에 코드를 이용하는 것에는 큰 문제가 없을 것 같지만....
일단 로그상 예상 범위 밖의 기록이 있어서 방식 자체가 혹시라도 유효하지 않은 방식인지..싶어서 질문 드립니다. :D
[guest]
2018-06-11 08시53분
질문을 정리해 보면, "start 지점과 QPF/1000보다 커진 지점 사이"의 시간은 1ms 근처로 나오는데, 그것을 timeGetTime에서는 15같은 값이 나온다는 건가요?
정성태
2018-06-11 11시35분
[질문드립니닷] 네 맞습니다. 수십회 반복하다보면 이따금씩 그런 값이 출력됩니다.
[guest]
2018-06-12 01시07분
혹시 그 코드를 제 컴퓨터에서도 실행해 보면 재현 가능할까요? 아니면 특정 PC에서만 그럴까요? 재현 가능하다면 (최소한의 재현 가능한) 프로젝트를 올려 주시면 살펴 보겠습니다.

어쨌든, QPF는 1ms가 지났다고 나오면서 동일 시간에 timeGetTime은 15ms가 나온다고 보고되고 있으니 굳이 설명해 보면 이럴 수는 있습니다.

int startQPC = CurrentQPF();
DWORD startTick = timeGetTime();

int endQPC = CurrentQPF();
// 바로 이 지점에서 스레드 스케쥴링이 발생해 15ms의 지연이 발생
DWORD endTick = timeGetTime();
정성태
2019-03-14 05시27분
[guest] 320ns보다 작은 수행시간을 측정하는 것은 불가능한 건가요?
[guest]
2019-03-14 10시00분
QPC로는 그렇고 TSC를 이용하면 (1/CPU클럭)이니까 더 낮아지겠죠. (참고로 이제는 100나노초까지 낮춰졌습니다.)
정성태
2020-02-11 11시08분
[개발자] 구글링해서 본 자료중에 가장 잘 정리된 글이네요. 감사합니다.
근데 헤깔리는 부분이 그럼 결국 QueryPerformanceFrequency 는 TSC를 내부적으로 사용하는 게 아닌건가요?
이 함수가 반환하는 값이 초당 TICK수라고 하셨는데 별도의 하드웨어 타이머를 이용하는 건가요?
CPU 클럭이랑은 상관이 없는 건지 궁금하네요.
[guest]
2020-02-11 06시49분
본문에서도 언급했지만, QPC가 모든 경우에 TSC를 사용하는 것은 아닙니다. (아마도, 근래에는 대부분의 PC에서 TSC를 사용할 것으로 생각되지만. ^^)
정성태
2020-08-07 04시02분
[바다코끼리] 이 블로그에서 "윈도우 운영체제의 시간 함수" 시리즈 글을 모두 정독하였습니다. 읽고 나니 너무 감사해서 눈물이 나려고 하네요.
정말 만족스럽고 유익한 내용입니다. 네이버, 구글에서 검색을 해보면 대부분 MSDN에서 API 명세 퍼오거나 깔짝깔짝 번역해둔게 대다수를 차지하는데
정말 양질의 블로그 간만에 잘 봤습니다. 이런 양질의 블로그를 운영하시는 분이 어느 분일까 싶어 검색해 봤더니 제니퍼소프트, 끄덕끄덕. 앞으로 또 찾아오겠습니다. 감사합니다!!
[guest]
2020-08-07 10시40분
이렇게 읽어 주신 것만으로 저 역시 감사드립니다. ^^ (사실 이런 유의 글을 쓸 때는 누가 읽기나 할까... 라는 생각을 종종 하게 됩니다.)
정성태

1  2  3  4  5  6  7  8  9  10  11  12  [13]  14  15  ...
NoWriterDateCnt.TitleFile(s)
13296정성태3/25/20233691Windows: 234. IsDialogMessage와 협업하는 WM_GETDLGCODE Win32 메시지 [1]파일 다운로드1
13295정성태3/24/20233954Windows: 233. Win32 - modeless 대화창을 modal처럼 동작하게 만드는 방법파일 다운로드1
13294정성태3/22/20234125.NET Framework: 2105. LargeAddressAware 옵션이 적용된 닷넷 32비트 프로세스의 가용 메모리 - 두 번째
13293정성태3/22/20234192오류 유형: 853. dumpbin - warning LNK4048: Invalid format file; ignored
13292정성태3/21/20234312Windows: 232. C/C++ - 일반 창에도 사용 가능한 IsDialogMessage파일 다운로드1
13291정성태3/20/20234716.NET Framework: 2104. C# Windows Forms - WndProc 재정의와 IMessageFilter 사용 시의 차이점
13290정성태3/19/20234222.NET Framework: 2103. C# - 윈도우에서 기본 제공하는 FindText 대화창 사용법파일 다운로드1
13289정성태3/18/20233418Windows: 231. Win32 - 대화창 템플릿의 2진 리소스를 읽어들여 자식 윈도우를 생성하는 방법파일 다운로드1
13288정성태3/17/20233516Windows: 230. Win32 - 대화창의 DLU 단위를 pixel로 변경하는 방법파일 다운로드1
13287정성태3/16/20233686Windows: 229. Win32 - 대화창 템플릿의 2진 리소스를 읽어들여 윈도우를 직접 띄우는 방법파일 다운로드1
13286정성태3/15/20234147Windows: 228. Win32 - 리소스에 포함된 대화창 Template의 2진 코드 해석 방법
13285정성태3/14/20233738Windows: 227. Win32 C/C++ - Dialog Procedure를 재정의하는 방법파일 다운로드1
13284정성태3/13/20233938Windows: 226. Win32 C/C++ - Dialog에서 값을 반환하는 방법파일 다운로드1
13283정성태3/12/20233480오류 유형: 852. 파이썬 - TypeError: coercing to Unicode: need string or buffer, NoneType found
13282정성태3/12/20233818Linux: 58. WSL - nohup 옵션이 필요한 경우
13281정성태3/12/20233720Windows: 225. 윈도우 바탕화면의 아이콘들이 넓게 퍼지는 경우 [2]
13280정성태3/9/20234463개발 환경 구성: 670. WSL 2에서 호스팅 중인 TCP 서버를 외부에서 접근하는 방법
13279정성태3/9/20234005오류 유형: 851. 파이썬 ModuleNotFoundError: No module named '_cffi_backend'
13278정성태3/8/20233962개발 환경 구성: 669. WSL 2의 (init이 아닌) systemd 지원 [1]
13277정성태3/6/20234583개발 환경 구성: 668. 코드 사인용 인증서 신청 및 적용 방법(예: Digicert)
13276정성태3/5/20234313.NET Framework: 2102. C# 11 - ref struct/ref field를 위해 새롭게 도입된 scoped 예약어
13275정성태3/3/20234666.NET Framework: 2101. C# 11의 ref 필드 설명
13274정성태3/2/20234255.NET Framework: 2100. C# - ref 필드로 ref struct 타입을 허용하지 않는 이유
13273정성태2/28/20233955.NET Framework: 2099. C# - 관리 포인터로서의 ref 예약어 의미
13272정성태2/27/20234211오류 유형: 850. SSMS - mdf 파일을 Attach 시킬 때 Operating system error 5: "5(Access is denied.)" 에러
13271정성태2/25/20234146오류 유형: 849. Sql Server Configuration Manager가 시작 메뉴에 없는 경우
1  2  3  4  5  6  7  8  9  10  11  12  [13]  14  15  ...