Windowsで高精度タイマを使う

#はじめに　
Windowsで高精度タイマを使う方法を記載します。
PC環境はWindows10 @ Corei5-5200U 2.20GHzです。

**PerformanceCounter(PC)**と**Waitable Timer**を利用します。

PerformanceCounterは高精度のタイムスタンプです。
Waitable Timerは100nsec単位で待ち時間を指定します。

# PerformanceCounter
## 周波数(QueryPerformanceFrequency;QPF)
カウントアップする周波数は[QueryPerformanceFrequency](https://msdn.microsoft.com/ja-jp/library/windows/desktop/ms644905(v=vs.85).aspx)で取得できます。
私の環境では2143477 Hzでした。周期に直すと約467nsecです。

```c
#include <Windows.h>

LARGE_INTEGER qpf;
if (!QueryPerformanceFrequency(&qpf)) {
	/* error */
}
```

## カウンタ値(QueryPerformanceCounter;QPC)
現在のカウンタ値は[QueryPerformanceCounter](https://msdn.microsoft.com/ja-jp/library/windows/desktop/ms644904(v=vs.85).aspx)で取得できます。

```c
#include <Windows.h>

LARGE_INTEGER qpc;
if (!QueryPerformanceCounter(&qpc)) {
	/* error */
}
```

以下の手順でQPCを秒に変換できます。

```c
#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](https://msdn.microsoft.com/ja-jp/library/windows/desktop/ms682492(v=vs.85).aspx)でタイマを作成します。
lpTimerAttributesは属性です。NULL指定できます。
bManualResetはmanual-reset timer(TRUE)にするか、synchronization timer(FALSE)にするかを指定します。
lpTimerNameはタイマ名です。NULL指定で省略できます。
タイマの削除はCloseHandleで行います。

```c
#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](https://msdn.microsoft.com/ja-jp/library/windows/desktop/ms686289(v=vs.85).aspx)でタイマをセットします。
hTimerはタイマのハンドルです。
pDueTimeはシグナルを送る時間を指定します。単位は100nsecです。マイナス値は相対時間を示します。プラス値は絶対時間を指定します。
lPeriodは周期を指定します。単位は1msecです。プラス値のみ有効です。周期タイマとして使用しない場合は0を指定します。
pfnCompletionRoutine/lpArgToCompletionRoutineはコールバック関数と引数を指定します。
fResumeはresume時の動作を指定します。

WaitForSingleObjectでシグナルの受信を待ちます。

```c
#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との差分で精度を評価しています。

```c
#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](https://msdn.microsoft.com/ja-jp/library/windows/desktop/dn553408(v=vs.85).aspx)
[Using Waitable Timer Objects](https://msdn.microsoft.com/ja-jp/library/windows/desktop/ms687008(v=vs.85).aspx)