성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] How can I tell whether two programs...
[정성태] The case of the fail-fast crashes c...
[정성태] Creating Docker multi-arch images f...
[정성태] BinaryFormatter removed from .NET 9...
[정성태] Extending the Windows Shell Progres...
[우광현] 와..... 범위를 잡았으니 클라이언트가 해당 범위를 확인해본다...
[정성태] 딱히, 그것 이상으로 더 설명할 내용이 없습니다. 동적 포...
[정성태] If Windows 3.11 required a 32-bit p...
[정성태] What is a hard error, and what make...
[괴물신인] 질문작성자인데 이 글을 이제봤네요 ㄷㄷ 이 글처럼 타입별로 인...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>윈도우 운영체제의 시간 함수 (5) - TSC(Time Stamp Counter)와 QueryPerformanceCounter</h1> <p> 지난 글에서 말로만 설명한 TSC를 코딩으로도 실습해 보겠습니다. ^^<br /> <br /> TSC는 CPU 차원에서 제공되는 카운터로 CPU가 리셋된 이후 동작한 CPU 사이클의 수를 "Time Stamp Counter"라는 64비트 레지스터에 보관하고 rdtsc/rdtscp 기계어로 접근할 수 있습니다. <br /> <br /> Visual C++의 경우 이를 위해 __rdtsc 내부 함수를 제공합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > __rdtsc (ReaD Time Stamp Counter) ; <a target='tab' href='https://learn.microsoft.com/en-us/cpp/intrinsics/rdtsc'>https://learn.microsoft.com/en-us/cpp/intrinsics/rdtsc</a> </pre> <br /> 아래는 간단한 사용법입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > #include "stdafx.h" #include <Windows.h> int main() { int count = 10; while (count -- > 0) { printf("%I64d\n", ::__rdtsc()); } return 0; } </pre> <br /> 별로 의미가 없지만 출력 결과는 이렇습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 173595388146208 173595388675124 173595388799672 173595389066244 173595389245210 173595389459434 173595389709418 ...[생략]... </pre> <br /> __rdtsc 외에도 ReadTimeStampCounter 함수를 제공하는데 이는 단순한 매크로 함수에 불과합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > #define ReadTimeStampCounter() __rdtsc() </pre> <br /> <hr style='width: 50%' /><br /> <br /> 출력 결과에서 본 것처럼, "CPU 사이클의 수"이기 때문에 여러분들의 CPU가 몇 GHz냐에 따라 이 수치를 밀리초 등의 단위로 바꾸는 방법이 달라지게 됩니다.<br /> <br /> 일단, 정말로 TSC가 "CPU 사이클의 수"인지 다음과 같은 코드로 대략 확인할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > #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; } </pre> <br /> 제 컴퓨터에서 출력 결과는 다음과 같습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > elapsed: 3408820574, 3.4088GHz elapsed: 3404523320, 3.4045GHz elapsed: 3408260224, 3.4083GHz elapsed: 3407562582, 3.4076GHz elapsed: 3407761180, 3.4078GHz elapsed: 3406738008, 3.4067GHz </pre> <br /> 실제로 제 컴퓨터는 3.4GHz의 i5 CPU인데 대략 비슷한 값을 보이고 있습니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 이전 글에서도 언급했지만, TSC 레지스터가 cpu cycle의 수를 담고 있기 때문에 이후 Turbo-Boost 기능이 추가된 CPU의 경우 이것이 문제가 됩니다. 실행 중에 동적으로 CPU 사이클이 3.4GHz에서 3.8GHz 등으로 변화할 수 있기 때문에 특정 함수의 성능을 측정하기 위해 사용한 __rdtsc의 반환 값을 어떻게 해석해야 할지 난감하게 된 것입니다.<br /> <br /> 이런 문제를 다행히 CPU 제조사에서 해결을 해주었는데, 동적으로 변화하는 CPU Cycle 수를 TSC 레지스터에 반영하지 않고 CPU SPEC에 명시된 (제 경우 3.4GHz) 사이클 기준으로 CPU 내부에서 적절하게 변환해 주는 기능을 추가한 것입니다. 이름하여 "Invariant TSC"라고 합니다.<br /> <br /> 실제로 <a target='tab' href='https://learn.microsoft.com/en-us/sysinternals/downloads/coreinfo'>sysinternals의 coreinfo.exe</a>를 이용해 자신의 CPU에 이런 기능이 적용되었는지 쉽게 확인할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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 <span style='color: blue; font-weight: bold'>TSC-INVARIANT * TSC runs at constant rate</span> xTPR * Supports disabling task priority messages ...[생략]... </pre> <br /> 다음의 글은, "Invariant TSC" 기능이 있는 환경에서 "Turbo Boost" On/Off에 상관없이 TSC의 값이 일관성 있게 나온다는 것을 테스트하고 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > rdtsc in the Age of Sandybridge ; <a target='tab' href='https://randomascii.wordpress.com/2011/07/29/rdtsc-in-the-age-of-sandybridge/'>https://randomascii.wordpress.com/2011/07/29/rdtsc-in-the-age-of-sandybridge/</a> </pre> <br /> 추가로 coreinfo.exe의 출력 결과 중에 "RDTSCP" 항목을 잠깐 설명해 보겠습니다. 지난 글에서 언급한 것처럼, 명령어의 실행 순서가 보장받지 못하는 환경에서도 rdtsc를 사용하면 값의 왜곡이 발생한다고 했는데요. 이런 경우의 대안으로 <a target='tab' href='http://stackoverflow.com/questions/2918113/cpuid-before-rdtsc'>직렬화가 수행되는 명령어</a>를 넣어주면 되는데 대표적인 예로 cpuid가 있어서 다음과 같이 한다고 합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > unsigned __int64 inline GetRDTSC() { __asm { ; Flush the pipeline XOR eax, eax <span style='color: blue; font-weight: bold'>CPUID</span> ; Get RDTSC counter in edx:eax <span style='color: blue; font-weight: bold'>RDTSC</span> } } </pre> <br /> CPU 제조사에서는 이를 하나로 묶는 명령어를 이후 추가하게 되었는데 그것이 바로 rdtscp입니다.<br /> <br /> 참고로, TSC 레지스터를 읽어들이는 것은 Ring 0 수준에서만 가능하도록 바꿀 수도 있습니다. 물론, Ring 3 수준에서 읽는 것을 막고 싶다면 그것을 수행하는 작업을 대행하는 device driver를 만들면 됩니다.<br /> <br /> 어쨌든, 대충 살펴보는 것만으로 TSC를 직접 사용하려면 환경에 따라 고려해야 할 것이 많다는 사실이 별로 탐탁지 않으실 것입니다. ^^<br /> <br /> <hr style='width: 50%' /><br /> <br /> 이런저런 문제로 인해, 마이크로소프트는 TSC를 직접 사용하는 것을 지양하고 대신 TSC의 단점들을 운영체제 차원에서 보완한 <a target='tab' href='https://learn.microsoft.com/en-us/windows/win32/api/profileapi/nf-profileapi-queryperformancecounter'>QueryPerformanceCounter</a>를 사용할 것을 권장하고 있으며 문서상으로도 <a target='tab' href='https://learn.microsoft.com/en-us/windows/win32/sysinfo/acquiring-high-resolution-time-stamps#general-faq-about-qpc-and-tsc'>QPC가 다중 코어/프로세서 및 하이퍼스레딩에서조차도 신뢰할 수 있는 값을 제공한다고 명시</a>하고 있습니다.<br /> <br /> QueryPerformanceCounter는 TSC와 개념이 비슷하므로 마찬가지로 사용 방법도 간단합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > #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; <a target='tab' href='https://learn.microsoft.com/en-us/windows/win32/api/profileapi/nf-profileapi-queryperformancefrequency'>QueryPerformanceFrequency</a>(&qpcRate); return qpcRate.QuadPart; } int main() { <a target='tab' href='https://learn.microsoft.com/en-us/windows/win32/api/timeapi/nf-timeapi-timebeginperiod'>timeBeginPeriod</a>(1); const DWORD msDuration = 1000; const int iterations = 6; const __int64 qpcFreqency = GetQPCFreq(); for (int i = 0; i < iterations; ++i) { <span style='color: blue; font-weight: bold'>__int64 qpcStart = GetQPCTime();</span> DWORD startTick = <a target='tab' href='https://learn.microsoft.com/en-us/windows/win32/api/timeapi/nf-timeapi-timegettime'>timeGetTime</a>(); for (;;) { DWORD tickDuration = timeGetTime() - startTick; if (tickDuration >= msDuration) { break; } } <span style='color: blue; font-weight: bold'>__int64 qpcElapsed = GetQPCTime() - qpcStart;</span> printf("%I64d / %I64d == %0.4f\n", qpcElapsed, qpcFreqency, ((double)qpcElapsed / qpcFreqency)); } timeEndPeriod(1); return 0; } </pre> <br /> (업데이트 2022-04-22: 위의 소스 코드는 이제 다르게 동작합니다. 보다 자세한 사항은 "<a href='https://www.sysnet.pe.kr/2/0/13035'>Windows 10부터 바뀐 QueryPerformanceFrequency, QueryPerformanceCounter" 글</a>에서 다루므로 참고하세요.)<br /> <br /> 위의 코드를 수행한 결과는 이렇습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 3327949 / 3328129 == 0.9999 3327319 / 3328129 == 0.9998 3329828 / 3328129 == 1.0005 3325995 / 3328129 == 0.9994 3329377 / 3328129 == 1.0004 3325932 / 3328129 == 0.9993 </pre> <br /> 거의 1초에 가까운 지연 시간을 확인할 수 있습니다. <br /> <br /> QueryPerformanceFrequency가 반환한 것은 초당 tick 수입니다. 따라서 만약 이 값이 (위의 출력 결과에서는 3,328,129) 3,125,000이라면, 주기 값 T = 1 / 3,125,000 = 0.000000320(320 nanoseconds)가 됩니다. 즉, QueryPerformanceCounter 값의 차이가 1이라면 (이론상) 320나노 초가 지난 시간임을 의미합니다. 당연하겠지만, 이런 시스템에서는 시간값의 정밀도를 320나노 초 이하로 내리는 것은 불가능합니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 알아본 김에 마지막으로 C++의 clock 함수를 설명해 보겠습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > clock ; <a target='tab' href='https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/clock'>https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/clock</a> </pre> <br /> 이 함수 역시 내부적으로는 (윈도우 환경에서라면) QueryPerformanceCounter를 사용하고, 따라서 (대부분의 경우) TSC를 사용하는 것과 다름없습니다. 실제로 Windows SDK가 설치된 경우 다음과 같은 경로를 통해 소스 코드를 보시면 확인할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // 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 (!<span style='color: blue; font-weight: bold'>QueryPerformanceCounter</span>(&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 (!<span style='color: blue; font-weight: bold'>QueryPerformanceFrequency</span>(&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; } </pre> <br /> 단지, clock 함수는 1초 당 1000 clocks가 되는 것을 기준으로 QueryPerformanceCounter의 단위를 좀 더 다듬었다고 보시면 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // time.h #define CLOCKS_PER_SEC ((clock_t)1000) </pre> <br /> (<a target='tab' href='http://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=1068&boardid=331301885'>첨부 파일은 이 글의 예제 코드를 포함</a>합니다.)<br /> <br /> <hr style='width: 50%' /><br /> <br /> 참고로, 이번 5부에 걸친 내용은 시스템 하위의 것을 포함하고 있기 때문에 언제든지 deprecated 될 수 있습니다.<br /> <br /> 실제로, 웹상에 있는 많은 자료들이 초기 TSC 구현의 것을 모델로 설명한 것들이 많아서 이제는 진실이 아닌 것이 되어버렸는데, (일례로 QueryPerformanceCounter는 스레드 친화도를 설정해야 정확한 값이 보장된다는 식) 이 글 역시 시간 앞에서 자유로울 수 없습니다. (음... 그러니까 미리... ^^ 쉴드를 쳐봅니다.)<br /> <br /> 마지막으로, 정리를 해보면.<br /> <br /> <ul> <li>GetTickCount/<a target='tab' href='https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-gettickcount64'>GetTickCount64</a>는 대충 20ms 정도의 별로 중요하지 않은 시간 간격을 측정하는 용도라면 좋습니다.</li> <li>timeGetTime은 1ms 단위의 시간 측정에 좋습니다. 대신 목적에 따라 timeBeginPeriod, timeEndPeriod를 이용해 정밀도를 조정해야 할 필요가 있습니다. (아울러 조정된 정밀도만큼, 전력 소비가 늘어난다는 단점)</li> <li>TSC는 높은 신뢰성을 요구하지 않는 환경에서는 정밀함과 속도를 만족합니다.</li> <li>QueryPerformanceCounter는 TSC의 단점을 보완하지만, 구형 컴퓨터에 설치된 Windows XP를 고려해야 한다면 timeGetTime을 사용합니다.</li> </ul> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
3110
(왼쪽의 숫자를 입력해야 합니다.)