LoginSignup
28
38

More than 5 years have passed since last update.

Windowsで高精度タイマを使う

Posted at

はじめに 

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;
}
console
qpf=2143477, qpc=1135677270075, sec=529829.464032

Waitable Timer

Waitable Timerを使うことで次のことを行えます。

  1. 一定間隔のイベントを発生させる。
  2. 絶対時間を指定してイベントを発生させる。
  3. 相対時間を指定してイベントを発生させる。

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程度です。

console
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

28
38
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
28
38