Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (kevin13@chol.net)
홈페이지
첨부 파일
[qpc.zip]    

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

윈도우 운영체제의 시간 함수 (1) - GetTickCount와 timeGetTime의 차이점
; http://www.sysnet.pe.kr/2/0/11063

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

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

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

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

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

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

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

__rdtsc (ReaD Time Stamp Counter)
; https://msdn.microsoft.com/ko-kr/library/twchhe95.aspx

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

#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;
}

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

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://msdn.microsoft.com/en-us/library/4e2ess30.aspx

이 함수 역시 내부적으로는 (윈도우 환경에서라면) 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을 사용합니다.




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





[최초 등록일: ]
[최종 수정일: 10/19/2016 ]

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

비밀번호

댓글 쓴 사람
 



2018-06-11 01시51분
[질문드립니닷] 안녕하세요, 또 한번 질문드릴게 있습니다.. ㅎㅎ..
저번에 말씀주신대로 쓰레드를 구성하고 우선순위를 최고 우선순위보다 높게 지정 한 후에
올려주신 소스의 구성도 살짝 바꿔보았는데요, 올려주신대로 start와 현재의 timeGetTime() 차가 1일때가 아닌
현재 QPC 값에서 start했을때의 QPC가 QueryPerformanceFrequency/1000 의 값보다 커졌을때로 바꿨습니다.
그런데 결과를 보면 동일하게 클럭값의 차를 통해 계산했을때 꾸준히(어쩌면 당연하게..?) 1ms 이지만
그때의 timeGetTime 값의 차를 비교해보면 0 혹은 1이 꾸준히 나오다가 아주 가끔 15 정도의 튀는 값이 있더군요.
이 방식이 유효한 시간 측정 방식이라고 볼수 있는 것인지에 대해서 질문드립니닷.

[손님]
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
[손님]
2018-06-11 08시53분
질문을 정리해 보면, "start 지점과 QPF/1000 보다 커진 지점 사이"의 시간은 1ms 근처로 나오는데, 그것을 timeGetTime에서는 15같은 값이 나온다는 건가요?
정성태
2018-06-11 11시35분
[질문드립니닷] 네 맞습니다. 수십회 반복하다보면 이따금씩 그런 값이 출력됩니다.
[손님]
2018-06-12 01시07분
혹시 그 코드를 제 컴퓨터에서도 실행해 보면 재현 가능할까요? 아니면 특정 PC에서만 그럴까요? 재현 가능하다면 (최소한의 재현 가능한) 프로젝트를 올려 주시면 살펴 보겠습니다.

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

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

int endQPC = CurrentQPF();
// 바로 이 지점에서 스레드 스케쥴링이 발생해 15ms의 지연이 발생
DWORD endTick = timeGetTime();

정성태

[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
11556정성태6/19/2018100.NET : 620. C# 7.3 - 구조체의 고정 크기를 갖는 fixed 배열 필드에 대한 직접 접근 가능파일 다운로드1
11555정성태6/18/2018110.NET : 619. C# 7.3 - 사용자 정의 타입에 fixed 적용 가능(Custom fixed)파일 다운로드1
11554정성태6/17/2018151.NET : 618. C# 7.3 - 자동 구현 속성에 특성 적용 가능(Attribute on backing field)
11553정성태6/15/2018162.NET : 617. C# 7.3 - 개선된 메서드 선택 규칙 3가지(Improved overload candidates)파일 다운로드1
11552정성태6/15/2018155.NET : 616. C# 7.3에서 개선된 문법 4개(Support == and != for tuples, Ref Reassignment, Constraints, Stackalloc initializers)파일 다운로드1
11551정성태6/14/2018152개발 환경 구성: 391. BenchmarkDotNet 사용 시 주의 사항
11550정성태6/13/2018160.NET : 615. BenchmarkDotNet으로 Span<T> 성능 측정
11549정성태6/13/2018149개발 환경 구성: 390. BenchmarkDotNet에서 생성한 BuildPlots.R 파일을 실행하는 방법
11548정성태6/13/2018162오류 유형 : 466. .NET Core + BenchmarkDotNet 실행 시 프레임워크를 찾지 못하는 문제
11547정성태6/13/2018203.NET : 614. BenchmarkDotNet 라이브러리 소개파일 다운로드1
11546정성태6/14/2018317.NET : 613. C# 7.2의 특징 - GC 및 메모리 복사 방지를 위한 struct 타입 개선 [4]파일 다운로드1
11545정성태6/11/2018259오류 유형 : 465. .NET Core 프로젝트를 Visual Studio에서 실행 시 System.BadImageFormatException 발생하는 경우 [1]
11544정성태6/10/2018203.NET : 612. C# 7.2 - 숫자 리터럴의 선행 밑줄과 뒤에 오지 않는 명명된 인수
11543정성태6/10/2018235.NET : 611. C# 7.2 - private protected 접근자 추가파일 다운로드1
11542정성태6/9/2018136개발 환경 구성: 389. Azure Web App 확장 예제 - Remove Custom Headers
11541정성태6/9/2018170개발 환경 구성: 388. Azure Web App 확장 배포 방법
11540정성태6/9/2018196개발 환경 구성: 387. Azure Web App 확장 예제 제작
11539정성태6/8/2018232.NET : 610. .NET Core 2.1 - Tiered Compilation 도입파일 다운로드1
11538정성태6/8/2018216.NET : 609. .NET Core 2.1 - 확장 도구(Tools) 관리
11537정성태6/8/2018204.NET : 608. C# - SmtpClient로 SMTP + SSL/TLS 서버를 이용하는 방법
11536정성태6/8/2018336.NET : 607. Microsoft Build 2018 - The future of C# 동영상 내용 정리 [1]파일 다운로드1
11535정성태6/7/2018331.NET : 606. C# - System.Span<T> 성능 [1]
11534정성태6/10/2018343.NET : 605. C# 7.2 - Span<T> [3]
11533정성태6/8/2018266.NET : 604. 포인터 형 매개 변수를 갖는 C++ DLL의 함수를 C#에서 호출하는 방법파일 다운로드1
11532정성태6/5/2018237.NET : 603. JSON의 escape sequence 문자 처리 방식
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...