3-2 高解像度タイマー

Windowsには1msより細かい精度で取得可能なカウンタAPIがありますが、
ここではこの高解像度カウンター関数について説明をしています。

■高解像度タイマーカウンタAPI

高解像度カウンター関数は以下の2つのみです。
これを使うと超高解像度でズレ無しの音ゲーが作れます。

Ⅰ. システムの1秒間のカウント数(周波数)を64bit値で取得
BOOL QueryPerformanceFrequency( LARGE_INTEGER *lpFrequency );
この関数ではシステムの周波数を取得します。
この値はかなり大きな数なので64bit精度を使用します。

関数が成功すると
TRUEが返り、lpFrequencyにその値を返します。
失敗すると
FALSEを返すので、もしこの関数が失敗した場合は、
そのコンピュータでは高解像度タイマーをサポートしていない事になります。
※多少古いPCでも対応していないということはほぼありえません

LARGE_INTEGERは以下のように定義されています。
typedef union _LARGE_INTEGER {
    struct {
        DWORD LowPart; 
        LONG  HighPart; 
    };
    LONGLONG QuadPart;
} LARGE_INTEGER, *PLARGE_INTEGER;
LARGE_INTEGERは64bit整数(LONGLONG)としてQuadPartを保持しています。
また見てわかるとおり、
union(自分的にうにょんと呼ぶ)指定なので、
LowPartとHighPartはこの64bit値を32bitで分けた値です。
(インテルCPUはリトルエンディアンなCPUなので上位バイトは後にきます)
呼び出した瞬間のカウント数を64bit値で取得
BOOL QueryPerformanceCounter( LARGE_INTEGER *lpPerformanceCount );
この関数で現在のカウント数を取得します。
関数が成功すると
TRUEが返り、失敗するとFALSEが返ります。
この関数で取得した値と、QueryPerformanceFrequency()で取得したシステムの
周波数とで計算することにより、1ms以下の精度で比較値を取ることが出来ます。



■経過カウントの算出

では起動してから呼び出した瞬間の時間は、いったい何秒なのかを測るプログラムを考えましょう。

まず最初に高精度タイマーAPIが使用できるかチェックするのと、ついでにPCの周波数も記憶しておきます。
これはQueryPerformanceFrequency()関数で一発なので簡単です。

// グローバル変数
BOOL bPerf;                 // 高解像度カウンターに対応しているか
LARGE_INTEGER mFreq;        // 1秒間のカウント数

BOOL Init( void )
{
    bPerf = QueryPerformanceFrequency( &sys );
    if( bPerf )
        return TRUE;

    // 非対応ならtimeGetTime()の精度とする
    sys.QuadPart = 1000;
    return FALSE;
}

この例では高解像度カウンターに対応していない場合、精度の低いtimeGetTime()で代用するようにしています。

ではスタートから指定時間の経過時間を取得してみましょう。
なおシステム周波数は上記のmFreqとして既に定義してあるとします。

void Proc( void )
{
    // 開始時のカウント値を取得
    LARGE_INTEGER start;
    QueryPerformanceCounter( &start );

    while( 1 ) {
        // 何かの処理// 現在のカウント値を取得
        LARGE_INTEGER now;
        if( bPerf ) {
            QueryPerformanceCounter( &now );            // 高解像度カウンタに対応している場合
        } else {
            now.QuadPart = timeGetTime();               // 高解像度カウンタに対応していない場合
        }

        double time = (double)(now.QuadPart - start.QuadPart) / (double)mFreq.QuadPart;
        Trace( "経過時間 : %f秒¥n",time );
    }
}


現在のカウントから開始時のカウントを引くと経過中のカウント数が取得できます。
これを1秒間のカウント数で割ってやればスタートからの時間が秒として取得出来ます。


なおプログラム中で
double型に変換しているのは、割った結果も高精度で取得するためです。
floatでは表現できる数値の幅が狭いのでここではdoubleを使用します。

本来はLONGLONG型を
doubleに変換するとデータの欠落が発生してしまう場合がありますが、
経過カウントでは差分を取っているのでそこまで大きな差は出ないだろうし、
システムカウント値でも大きくて200万くらいなので、この計算はほぼ問題無いと考えて大丈夫です。


■WinXPとマルチコアの問題

超高精度なカウンタが使えるとウハウハな気分になりますが、実は高解像度カウンターには致命的な問題があります。

1つ目はWinXPで動作させた場合で、このカウンタはCPU内にあるクロックをそのままカウントアップするもので、
最近のCPUは負荷が低いとクロックを抑える機能がありますが、
これによりクロックが変化するとカウントも変化してしまうといった問題があります。
そのため常に負荷をかけた状態にするような対策が必要となります。
なおこの問題はVistaから修正されており、それ以降のOSであればクロックダウンによる問題は無くなりました。

2つ目は最近のCPUは1つのCPUに2つ以上のコアが入っているマルチコアが一般的ですが、
高解像度カウンタは実はコアごとに持っており、しかもそれぞれ開始のタイミングが微妙に異なるため、
どのコアのカウンタを取るかによって誤差が発生します。

QueryPerformaceCounter()を呼び出した場合、実行されるコアがその都度異なる可能性があり、
これにより取得したカウント値を使って画面の表示をすると、
何故か微妙に処理落ちのようにカク付いたりといった問題が発生します。
この場合はQueryPerformanceCounter()を使用するスレッドを固定するといった対策が必要となります。
※特に計測分野ではこの誤差がかなり問題になっているようです