2012年8月12日日曜日

時間の測定

アニメーションなどの定期的に実行する処理を作る時は勿論、自分が何か変な事をやって異常な時間コストを払うプログラムを書いていないか等をチェックする時にも時間の計測が有効です。

windowsで高精度な時間の測定が行える関数は
QueryPerformanceCounter() です(以後Counterと呼ぶことにする)。
ただしこの関数で得られる時間は実行環境によって精度が異なるため
QueryPerformanceFrequency()(以後Frequencyと呼ぶことにする)
という関数を一度は呼び出して、その環境における精度を取得しておく必要があります。この精度は周波数と呼ばれ、Counterが1秒間に何回カウントするのかを示します。
Counterで得られる値は、システム起動後から刻一刻と加算されていく数字なので、何かの時間を計るにはCounterを2回呼び出して前後の差を求めます。そしてその結果をFrequencyで割れば秒単位の時間が求まります。
なお、これらの関数の利用には
#include <windows.h>のインクルードが必要です。

例)-------------
//精度の取得
LARGE_INTEGER freq;
QueryPerformanceFrequency(&freq);

//現在の時間を取得
LARGE_INTEGER start;
QueryPerformanceCounter(&start);

/*ここに測定する処理の記述*/

//もう一度時間を取得
LARGE_INTEGER end;
QueryPerformanceCounter(&end);
double time = double (end.QuadPart - start.QuadPart) / double(freq. QuadPart);
---------------------
以上のように記述すると2回のCounter呼び出しの間に何秒間経過したかがdoubleのtimeという変数に入ります。
CounterやFrequencyが取る引数はLARGE_INTEGERという特殊な型であり、このままでは整数として扱えないので.QuadPartというメンバにアクセスして利用します。

以下は時間の測定結果をミリ秒単位で戻すクラスの定義例とその利用例です。
------------------------------
#include <windows.h>//QueryCounter、及びSleep()用
#include <sstream> //ods()用


//デバッグウインドウへ出力
template < typename T, typename T2 > void ods( T tep, T2 tep2, T tep3)
{
std::wstringstream ss;
std::wstring st;

ss << tep << tep2<< tep3 << L"\n";
st = ss.str();
OutputDebugString(st.c_str());
};

// ミリ秒単位でStart()からEnd()までの時間を取得するクラス
class QueryCounter
{
public:
QueryCounter(void)
{
LARGE_INTEGER freq;
QueryPerformanceFrequency(&freq);
dFreq_ = (double)freq.QuadPart / 1000.0;
}

void Start()
//時間計測の開始時に呼んで下さい
{
QueryPerformanceCounter(&start_);
}

double End()
//時間計測の終了時に呼んで下さい
{
LARGE_INTEGER end;
QueryPerformanceCounter(&end);
return (double (end.QuadPart - start_.QuadPart) / dFreq_ );
}

private:
LARGE_INTEGER start_;
double dFreq_;
};

int main()
{

QueryCounter qc;
qc.Start();
Sleep(10);
ods(L"Sleep(10): ", qc.End(),L" ㍉秒");

double d;
qc.Start();
Sleep(5);
d = qc.End();
ods(L"Sleep(5) : ", d ,L" ㍉秒");

getchar();
return 0;
}
-------実行結果-------------
Sleep(10): 9.99923 ㍉秒
Sleep(5) : 4.41183 ㍉秒
----------------------------

SleepはWindowsでミリ秒単位で休止時間を指定できる関数ですが、その精度はミリ秒とはいかない様です。(そもそも関数呼び出しのオーバーヘッドやCPUやコンパイラが行う最適化の都合などで極小の処理時間を計るのは難しい作業の様な・・・。)

QueryPerformanceCounterは万能か?

Windowsで経過時間を取得する方法は沢山あるのですが、その中でなぜこの関数が良いのでしょうか?
ゲームのタイミングとマルチコア プロセッサ
というMSのページでこの関数についての説明があります。

DXUTや他のWindowsのライブラリでも内部でこの関数を使っているっぽいので、益々この関数の重要性がまして来ると考えられる昨今、この関数に対応しない環境は減少に向かうのではないでしょうか?

WindowsXPでこの関数が使えなかったので色々と調べた人のページ
が参考になるかも知れません。

我らはDirectX11以降=vista以降の環境を対象にプログラミングをするからXPの話は問題にならないのだ!
と考えるのも良いし(てゆうかXPでも大体使えるだろうし)、一応、使えなさそうな時は

timeGetTime

を使うように処理を切り替える様な安全弁を設けておいても良いでしょう。
timeGetTime()を使うには以下のヘッダとライブラリが必要です。

#include <mmsystem.h>
#pragma comment(lib, "winmm.lib")

timeGetTime() 関数を比較的精度よく使う時は、またちょっとした手法が必要だったりします。WindowsNT系のOSでは
timeBeginPeriod()という関数で精度を指定しないと分解能が悪かったりするとうい話です。(ビスタや7も含め、Windows2000以降ぐらいからNT系のOSだったりします。)

引数はミリ秒単位で、最小1ミリ秒まで指定できます。
そして使い終わったら
timeEndPeriod()を呼ぶ必要があります。

timeBeginPeriod(1);
を自作タイマークラスのコンストラクタとかで呼んでおいて
timeEndPeriod(1);
をデストラクタで呼ぶ等すればいいのではないでしょうか?

環境によってはtimeBeginPeriodでタイマーの精度を上げるとCPUの負荷が増すなどのちょっとした副作用があるっぽいです。

0 件のコメント: