QueryPerformanceCounterを実時間計測には使えない

とまあ、何とも変な題名ですが、これはその通りなのでこれについて文句を書いてからコードを修正しようかと思います。

(2013/02/17 追記:OSによりこの状態は異なります。このページの最後に書いてある追記も参照してください)

ほかのページにも書いてありますが、Windowsで時間計測をするには以下のAPIを呼び出すのが普通とされています。

  • GetTickCount,GetTickCount64 (GetTickCount64はVista以降)
  • timeGetTime (+timeBeginPeriod/timeEndPeriod)
  • QueryPerformanceCounter,QueryPerformanceFrequency

補助的にはこんなやり方もありです。

  • DirectSoundで無音バッファを再生させてそのカーソル位置で判定(IDirectSoundNotifyを使うと効果的)

GetTickCountとtimeGetTimeは最低単位が1msなのでそれ以上細かく測れません。

(GetTickCountの分解能はtimeGetTime+timeBeginPeriodより甘くなるのも注意。timeBeginPeriodを使うとCPU使用率が上昇する代わりに1msの分解能まで出せる)

が、ふつうはこちらを使う方が一般的です。

今回は動画再生が目的だったので、fpsで正確に描画しよう思ってQueryPerformanceCounter経由で組んだのですが・・・

この見通しがとんでもなく甘かったわけです・・・

QueryPerformanceCounterは何を取得してくるのか?

MSDNの説明では

高分解能パフォーマンスカウンタが存在する場合、そのカウンタの現在の値を取得します。

となっています。問題はこの「高分解能パフォーマンスカウンタ」が何者か?ということです。

いくつかのパターンが考えられますが、通常は以下のどちらかになります。

  • CPUクロックそのもの(rdtsc命令を利用したクロック取得)
  • M/B上に積んであるリファレンスクロックを利用したもの

このとき、高分解能パフォーマンスカウンタが前者のパターン=CPUクロックだったときは、とんでもない現象が起こります。

QueryPerformanceFrequencyは何を取得してくるのか

MSDNの説明では

高分解能パフォーマンスカウンタが存在する場合、そのカウンタの周波数(更新頻度)を取得します。システムが動作している間は、周波数を変更できません。

となっています。「高分解パフォーマンスカウンタ」が後者の場合はあまり問題はありません。が、前者の時、この説明の最後の文「周波数を変更できません」によって問題が発生します。

今のCPUのほとんどはOSが起動してからもCPUクロックを動的に変更するようになっている

ここまで読めば想像できると思いますが、固定CPUクロックならこの命令で問題を起こすことはマルチプロセッサの取得ミスくらいしかないですが、

動的変更になっている今日では初期の周波数(これはCPUの最大クロックであることがほとんど)をQueryPerformanceFrequencyは返してくるわけです。

ということは・・・・

QueryPerformanceCounterで差分を測っても正しい時間に戻せない

実際にこんなコードを書いてみればわかります。

#include <windows.h>
#include <stdio.h>
int main(void)
{
	LARGE_INTEGER liBegin,liEnd,liFreq;
	QueryPerformanceCounter(&liBegin);
	Sleep(1000);
	QueryPerformanceCounter(&liEnd);
	QueryPerformanceFrequency(&liFreq);
	printf("%f\n",(double)(liEnd.QuadPart - liBegin.QuadPart) / (double)liFreq.QuadPart);
	return 0;
}

Sleepは正確に対象時間中断するわけではないですが、対象時間以上は中断するようになっている場合がほとんどだと思います。

これを実行してほぼきれいに1.0と表示される場合はそのマシンではQueryPerformanceCounterが使えるか、ほかのプロセスの負荷でCPUクロックが引き上げられていたのだと思われます。

ちなみに、私のマシン(Core2Quad Q8400 + P5Q)では、無負荷時に0.5という値が平均して出ていましたし、高負荷時(ベンチマークによる強制負荷)では1.0付近の値が出ていました。

これがTurbo BoostがついているCPUだとどうなるのやら・・・

つまり、負荷が変動するような環境でQueryPerformanceCounterを使っても正しい実時間は不明ということになります。

実時間でなくていい場合(計算にかかったクロック数を比較するような場合)では使用してもそれなりの効果を得られる「場合もある」わけです。

もちろん、QueryPerformanceFrequencyがそのときの正しい1秒あたりのカウント数を返す仕様だったとしても、負荷が途中で変わるような場合には結局実時間変換ができないわけですね。

というわけでこれを調べるためにいらない時間を使ってしまったので、さっさと実装に戻ることにします・・・。

2013/02/17 追記

この記事を書いたときには調べていませんでしたが、こちらで紹介し直していることがあります。

簡易的に書いておきますと

  • WinXPではQueryPerformance系はCPUクロックを返すので上記の現象が起こる
  • WinVista以上でHPETが有効の時はQueryPerformance系はHPETより時間を取得するようになっているのでほとんど問題はない
  • ほぼすべてのPCにはHPETで参照されるクロックが実装されている

となっています。WinVista以前を無視するのであればそこまで「使えない」と言うことはないと思われますが、注意は必要です。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

この記事のトラックバック用URL