#はじめに
Windowsで高精度タイマを使う方法を記載します。
PC環境はWindows10 @ Corei5-5200U 2.20GHzです。
PerformanceCounter(PC)とWaitable Timerを利用します。
PerformanceCounterは高精度のタイムスタンプです。
Waitable Timerは100nsec単位で待ち時間を指定します。
PerformanceCounter
周波数(QueryPerformanceFrequency;QPF)
カウントアップする周波数はQueryPerformanceFrequencyで取得できます。
私の環境では2143477 Hzでした。周期に直すと約467nsecです。
#include <Windows.h>
LARGE_INTEGER qpf;
if (!QueryPerformanceFrequency(&qpf)) {
/* error */
}
カウンタ値(QueryPerformanceCounter;QPC)
現在のカウンタ値はQueryPerformanceCounterで取得できます。
#include <Windows.h>
LARGE_INTEGER qpc;
if (!QueryPerformanceCounter(&qpc)) {
/* error */
}
以下の手順でQPCを秒に変換できます。
#include "stdafx.h"
#include <Windows.h>
int main()
{
LARGE_INTEGER qpf;
if (!QueryPerformanceFrequency(&qpf)) {
/* error */
}
LARGE_INTEGER qpc;
if (!QueryPerformanceCounter(&qpc)) {
/* error */
}
printf("qpf=%lld, qpc=%lld, sec=%f\n", qpf.QuadPart, qpc.QuadPart, (double)qpc.QuadPart/(double)qpf.QuadPart);
getchar(); /* wait user input */
return 0;
}
qpf=2143477, qpc=1135677270075, sec=529829.464032
Waitable Timer
Waitable Timerを使うことで次のことを行えます。
- 一定間隔のイベントを発生させる。
- 絶対時間を指定してイベントを発生させる。
- 相対時間を指定してイベントを発生させる。
1.についてはmsec単位での指定となります。精度を良くするには2./3.を使う必要があります。
ここではマルチメディアの再生に使いやすい3.について記載します。
作成(CreateWaitableTimer)
CreateWaitableTimerでタイマを作成します。
lpTimerAttributesは属性です。NULL指定できます。
bManualResetはmanual-reset timer(TRUE)にするか、synchronization timer(FALSE)にするかを指定します。
lpTimerNameはタイマ名です。NULL指定で省略できます。
タイマの削除はCloseHandleで行います。
#include "stdafx.h"
#include <Windows.h>
int main()
{
HANDLE timer = NULL;
timer = CreateWaitableTimer(
NULL, // LPSECURITY_ATTRIBUTES lpTimerAttributes,
TRUE, // BOOL bManualReset,
NULL // LPCTSTR lpTimerName
);
if (timer == NULL) {
/* error */
}
if (!CloseHandle(timer)) {
/* error */
}
getchar(); /* wait user input */
return 0;
}
タイマセット(SetWaitableTimer)
SetWaitableTimerでタイマをセットします。
hTimerはタイマのハンドルです。
pDueTimeはシグナルを送る時間を指定します。単位は100nsecです。マイナス値は相対時間を示します。プラス値は絶対時間を指定します。
lPeriodは周期を指定します。単位は1msecです。プラス値のみ有効です。周期タイマとして使用しない場合は0を指定します。
pfnCompletionRoutine/lpArgToCompletionRoutineはコールバック関数と引数を指定します。
fResumeはresume時の動作を指定します。
WaitForSingleObjectでシグナルの受信を待ちます。
#include "stdafx.h"
#include <Windows.h>
int main()
{
HANDLE timer = NULL;
timer = CreateWaitableTimer(
NULL, // LPSECURITY_ATTRIBUTES lpTimerAttributes,
TRUE, // BOOL bManualReset,
NULL // LPCTSTR lpTimerName
);
if (timer == NULL) {
/* error */
}
LARGE_INTEGER interval;
interval.QuadPart = -10 * 1000 * 1; /* unit:100nsec, wait xx msec */
if (!SetWaitableTimer(
timer, // HANDLE hTimer,
&interval, // LARGE_INTEGER *pDueTime,
0, // LONG lPeriod,
NULL, // PTIMERAPCROUTINE pfnCompletionRoutine,
NULL, // LPVOID lpArgToCompletionRoutine,
FALSE // BOOL fResume
)) {
/* error */
}
if (WaitForSingleObject(timer, INFINITE) != WAIT_OBJECT_0) {
/* error */
}
if (!CloseHandle(timer)) {
/* error */
}
getchar(); /* wait user input */
return 0;
}
精度評価
10usec,100usec,1msec,10msec,100msecの時間を指定して、それぞれ100回タイマを実行します。
タイマの前後でQPCを取得してその差分を正解として、指定した待ち時間とQPCとの差分で精度を評価しています。
#include "stdafx.h"
#include <Windows.h>
#include <math.h>
#include <float.h>
LARGE_INTEGER qpf;
HANDLE timer = NULL;
void precision_check(int N, int usec)
{
LARGE_INTEGER interval, qpc_before, qpc_after;
interval.QuadPart = -10 * usec; /* unit:100nsec */
double sum = 0, max = DBL_MIN, min = DBL_MAX, ave = 0, sd = 0;
double *buf = new double[N];
for (int i = 0; i < N; i++) {
if (!QueryPerformanceCounter(&qpc_before)) {
/* error */
}
if (!SetWaitableTimer(
timer,
&interval,
0, // LONG lPeriod,
NULL, // PTIMERAPCROUTINE pfnCompletionRoutine,
NULL, // LPVOID lpArgToCompletionRoutine,
FALSE // BOOL fResume
)) {
/* error */
}
if (WaitForSingleObject(timer, INFINITE) != WAIT_OBJECT_0) {
/* error */
}
if (!QueryPerformanceCounter(&qpc_after)) {
/* error */
}
INT64 elapsed_qpc = qpc_after.QuadPart - qpc_before.QuadPart;
double elapsed_msec = (double)elapsed_qpc * 1000.0 / (double)qpf.QuadPart;
buf[i] = elapsed_msec;
sum += elapsed_msec;
if (max < elapsed_msec)
max = elapsed_msec;
if (elapsed_msec < min)
min = elapsed_msec;
}
ave = sum / N;
for (int i = 0; i < N; i++) {
sd += pow(ave - buf[i], 2.0);
}
sd /= N - 1;
sd = sqrt(sd);
delete[] buf;
printf("msec=%f, N=%d, ave=%f, sd=%f, max=%f, min=%f\n", (double)usec/1000.0, N, ave, sd, max, min);
}
int main()
{
if (!QueryPerformanceFrequency(&qpf)) {
/* error */
}
timer = CreateWaitableTimer(
NULL, // LPSECURITY_ATTRIBUTES lpTimerAttributes,
TRUE, // BOOL bManualReset,
NULL // LPCTSTR lpTimerName
);
if (timer == NULL) {
/* error */
}
precision_check(100 /* N */, 10 /* usec */);
precision_check(100 /* N */, 100 /* usec */);
precision_check(100 /* N */, 1 * 1000 /* usec */);
precision_check(100 /* N */, 10 * 1000 /* usec */);
precision_check(100 /* N */, 100 * 1000 /* usec */);
if (!CloseHandle(timer)) {
/* error */
}
getchar(); /* wait user input */
return 0;
}
msecはタイマ指定時間(msec)、Nはサンプル数、aveは平均値、maxは最大値、minは最小値、sdは標準偏差です。
0.5msec以下のタイマ待ちはできていないことが分かります。sdは0.1~0.2msec程度です。
msec=0.010000, N=100, ave=0.498848, sd=0.018013, max=0.569635, min=0.403083
msec=0.100000, N=100, ave=0.499632, sd=0.011488, max=0.549574, min=0.454868
msec=1.000000, N=100, ave=1.168769, sd=0.239674, max=1.514828, min=0.585031
msec=10.000000, N=100, ave=10.036156, sd=0.113348, max=10.506761, min=9.865280
msec=100.000000, N=100, ave=100.180366, sd=0.136277, max=100.529187, min=99.870444
#references
Acquiring high-resolution time stamps
Using Waitable Timer Objects