計時の精度・分解能
QueryPerformanceCounter関数を用いた場合はこちら
GetTickCount関数とQueryPerformanceCounter関数の比較
Windowsでは、時間・時刻をミリ秒単位で返す関数が用意されているが、マルチタスクOSであるため、反応時間などの測定において実行タスクの切り替えに伴う計時誤差の可能性がある。これを調べるため時間の増加量を記録する次のループを用意した。
do {
t1
= GetTickCount();
if (t1 > t0){
x[cnt++]
= t1 - t0;
t0
= t1;
}
} while (cnt < N);
上のコードにおいて、関数GetTickCount( )はシステムがスタート(PCの電源がオン)してからの経過時間をミリ秒単位で返すものである(岡本安晴「大学生のための心理学VC++プログラミング入門」勁草書房、p.234, p.237参照)。マルチタスクによる計時誤差がない場合は、「t1 > t0」における増分t1 – t0 は1ミリ秒となるはずであるが、マルチタスクによる増分があると1ミリ秒より大きい値となる。上のコードを実行して100回(N=100)の増分量を記録し、出力するプログラムがCheckTime.exeである。このプログラムはVisual C++2008コンソールアプリケーションとして以下のように用意されている。
=====================================
#include "stdafx.h"
#include "Windows.h"
using namespace System;
int main(array<System::String ^> ^args)
{
const int N = 100;
int x[N];
int cnt = 0;
int t0, t1, ckt0, ckt1;
ckt0
= t0 = GetTickCount();
do {
t1
= GetTickCount();
if (t1 > t0){
x[cnt++]
= t1 - t0;
t0
= t1;
}
}
while (cnt < N);
ckt1
= t1;
for (int i = 0; i
< N; i++){
Console::Write((x[i].ToString()
+ "ms")->PadLeft(7));
if (i % 10 == 9) Console::WriteLine();
}
Console::WriteLine();
Console::WriteLine("時間増加時の平均間隔= " + ((ckt1 - ckt0) / double(N)).ToString() + "ms");
Console::WriteLine();
Console::WriteLine("Enterキーを押して終了。");
Console::ReadLine();
return 0;
}
====================================
プログラムCheckTime.exeは.NET Framework 3.5対応である。プログラム名CheckTime.exeをクリックしてダウンロード・実行すると図1のように結果が表示される。
図1
図1では、増分がすべて15ミリ秒以上である。計時においてマルチタスクの切り替えによる誤差が発生する場合は、最大16ミリ秒ぐらいの誤差の伴う可能性がある。
プログラムのソースコードファイルなどをまとめた圧縮ファイルCheckTime.zipは、圧縮ファイル名CheckTime.zipをクリックしてダウンロードしたものをマウスの右ボタンでクリックして表示されるメニュから「解凍」あるいは「展開」を選ぶと解凍するすることができる。解凍したファイルはVisual C++2008などで開くことができる。
QueryPerformanceCounter関数を用いた場合
上記GetTickCountは分解能が悪い、QueryPerformanceCounterを使うべきであるという指摘があったので(羽山 博@有限会社ローグ・インターナショナル; メーリングリストfpr,2010.3.23)、以下のコードで確かめてみた。
=================================
#include "stdafx.h"
#include "Windows.h"
using namespace System;
bool ck;
LARGE_INTEGER L_I;
double f;
const double c = Math::Pow(2.0, 32.0);
double MyGetTime(){
QueryPerformanceCounter(
&L_I );
double v = L_I.HighPart * c + L_I.LowPart;
return 1000000.0 * v / f;
}
int MyGetTimeMS(){
return int(0.001 *
MyGetTime());
}
void wait5000(){
double a = MyGetTime();
do { ; } while
(MyGetTime() < a + 5000.0);
return;
}
int main(array<System::String ^> ^args)
{
ck
= QueryPerformanceFrequency( &L_I );
if (ck) {
Console::WriteLine("QueryPerformanceFrequency succeeded.");
}else{
Console::WriteLine("QueryPerformanceFrequency failed.");
Console::ReadLine();
return 9;
}
Console::WriteLine(L_I.HighPart.ToString()
+ "/" + L_I.LowPart.ToString());
f
= L_I.LowPart;
const int N = 100;
double
x[N];
int cnt = 0;
double t0, t1, ckt0, ckt1;
ckt0
= t0 = MyGetTime();
do {
t1
= MyGetTime();
if (t1 > t0){
x[cnt++]
= t1 - t0;
t0
= t1;
}
}
while (cnt < N);
ckt1
= t1;
for (int i = 0; i
< N; i++){
Console::Write((x[i].ToString("F3") + "microsec")->PadLeft(15));
if (i % 5 == 4) Console::WriteLine();
}
Console::WriteLine();
Console::WriteLine("計時における増加時の平均間隔= " + ((ckt1 - ckt0) / double(N)).ToString() + "microsec");
Console::WriteLine();
cnt
= 0;
ckt0
= t0 = MyGetTimeMS();
double a = 2.0;
do {
t1
= MyGetTimeMS();
if (t1 > t0){
x[cnt++]
= t1 - t0;
}
t0
= t1;
}
while (cnt < N);
ckt1
= t1;
for (int i = 0; i
< N; i++){
Console::Write((x[i].ToString()
+ "ms")->PadLeft(15));
if (i % 5 == 4) Console::WriteLine();
}
Console::WriteLine();
Console::WriteLine("計時における増加時の平均間隔= " + ((ckt1 - ckt0) / double(N)).ToString() + "ms");
Console::WriteLine();
Console::WriteLine("Enterキーを押して終了。");
Console::ReadLine();
return 0;
}
===================================
実行結果は下記のようになった。
=================================
QueryPerformanceFrequency succeeded.
0/14318180
1.187microsec 0.768microsec 0.698microsec 0.698microsec 0.698microsec
0.698microsec 0.698microsec 0.698microsec 0.698microsec 0.908microsec
0.629microsec 0.698microsec 0.698microsec 0.698microsec 0.768microsec
0.629microsec 0.768microsec 0.698microsec 0.768microsec 0.629microsec
0.768microsec 0.698microsec 0.698microsec 0.698microsec 0.698microsec
0.698microsec 0.698microsec 0.698microsec 0.698microsec 0.698microsec
0.698microsec 0.629microsec 0.698microsec 0.768microsec 0.698microsec
0.698microsec 0.629microsec 0.698microsec 0.698microsec 0.698microsec
0.629microsec 0.698microsec 0.698microsec 0.698microsec 0.629microsec
0.698microsec 0.698microsec 0.698microsec 0.629microsec 0.768microsec
0.698microsec 0.698microsec 0.629microsec 0.698microsec 0.698microsec
0.698microsec 0.629microsec 0.698microsec 0.698microsec 0.698microsec
0.698microsec 0.698microsec 0.698microsec 0.698microsec 0.629microsec
0.768microsec 0.698microsec 0.698microsec 0.768microsec 0.768microsec
0.698microsec 0.698microsec 0.698microsec 0.698microsec 0.698microsec
0.698microsec 0.698microsec 0.698microsec 0.698microsec 0.629microsec
0.698microsec 0.768microsec 0.698microsec 0.768microsec 0.698microsec
0.698microsec 0.698microsec 0.698microsec 0.698microsec 0.698microsec
0.698microsec 0.698microsec 0.698microsec 0.698microsec 0.698microsec
0.629microsec 0.698microsec 0.768microsec 0.698microsec 0.768microsec
計時における増加時の平均間隔 = 0.706095333099365microsec
1ms 1ms 1ms 1ms 1ms
1ms 1ms 1ms 1ms 1ms
1ms 1ms 1ms 1ms 1ms
1ms 1ms 1ms 1ms 1ms
1ms 1ms 1ms 1ms 1ms
1ms 1ms 1ms 1ms 1ms
1ms 1ms 1ms 1ms 1ms
1ms 1ms 1ms 1ms 1ms
1ms 1ms 1ms 1ms 1ms
1ms 1ms 1ms 1ms 1ms
1ms 1ms 1ms 1ms 1ms
1ms 1ms 1ms 1ms 1ms
1ms 1ms 1ms 1ms 1ms
1ms 1ms 1ms 1ms 1ms
1ms 1ms 1ms 1ms 1ms
1ms 1ms 1ms 1ms 1ms
1ms 1ms 1ms 1ms 1ms
1ms 1ms 1ms 1ms 1ms
1ms 1ms 1ms 1ms 1ms
1ms 1ms 1ms 1ms 1ms
計時における増加時の平均間隔 = 1ms
Enterキーを押して終了。
=================================
確かに精度の向上が見られる。
実行形式のプログラムはCheckTime1.exeであり、ソースコードファイルなどをまとめた圧縮ファイルはCheckTime1.zipである。
GetTickCount関数による計時とQueryPerformanceCounter関数による計時を比べるプログラムCompTimers.exe(.NET Framework 3.5対応)を作成した。このプログラムCompTimers.exeでは、関数MyGetTimeを
double MyGetTime(){
QueryPerformanceCounter(
&L_I );
double v = L_I.HighPart * c + L_I.LowPart;
return 1000.0 * v / f;
}
と用意してQueryPerformanceCounter関数によるミリ秒単位での計時を行っている。Startボタンのクリックにより、
bt1
= MyGetTime();
bt2 = GetTickCount();
上のMyGetTime関数とGetTickCount関数による計時を行っている。Stopボタンのクリックで
et1
= MyGetTime();
et2
= GetTickCount();
richTextBox1->AppendText("\n\nQueryPerformanceCounter = " + int(et1-bt1).ToString() + "ms"
+
"\nGetTickCount = " + int(et2-bt2).ToString() + "ms");
と2つの関数による計時を行った後、Startボタンクリック時からStopボタンクリック時までの経過時間をリッチテキストボックスに書き出している。
プログラムの実行例を図2に示す。
図2
2つの関数による計時に差のあることが示されている。
プログラムのソースコードファイルなどは圧縮ファイルCompTimers.zipにまとめた。ファイル名CompTimers.zipをクリックしてダウンロードしたものをマウスの右ボタンでクリックすると表示されるメニュから「解凍」あるいは「展開」を選ぶと解凍できる。解凍したファイルはVisual C++2008などで開くことができる。
高分解能計時用クラス型MyTimer
高分解能計時用関数QueryPerformanceCounterを用いた計時用クラス型MyTimerをヘッダーファイルfinetimer.hに用意した。ソースコードを以下に示す。
=============================================
//
// A
fine resolution timer. Y.
Okamoto, 2010.03
//
#ifndef FINETIMER_CK
#define FINETIMER_CK
#include <Windows.h>
using namespace System;
namespace mytimer {
class MyTimer {
public:
LARGE_INTEGER
L_I;
bool ck;
double f, m_d_f;
double c;
MyTimer():
ck(false) {
if ( QueryPerformanceFrequency( &L_I ) ) {
ck
= true;
c
= Math::Pow(2.0, 32.0);
f
= L_I.LowPart + c * L_I.HighPart; // counts/sec
m_d_f
= 1000000.0 / f; // microseconds/count
}
}
//
In microsecond unit
double time(){
if (ck) {
QueryPerformanceCounter(
&L_I );
double v = L_I.HighPart * c + L_I.LowPart;
return v * m_d_f;
}
else {
return 0.0;
}
}
// In
millisecond unit
int timems(){
return Math::Round(time() * 0.001);
}
};
}
#endif
=============================================
クラス型MyTimerを用いるときは、例えば、
MyTimer tm;
とMyTimer型変数tmを定義して、
tm.time(); // マイクロ秒単位
tm.timems(); // ミリ秒単位
とメソッドtime()あるいはtimems()を呼び出すと、その時のPCの計時の値がマイクロ秒単位あるいはミリ秒単位で返される。クラス型MyTimerはヘッダーファイルfinetimer.hに全て宣言されていて、ソースファイルはない。したがって、MyTimerを利用するときは、ヘッダーファイルfinetimer.hの#includeと名前空間mytimerの指定を行えばよい。
上のヘッダーファイルfinetimer.hを用いたプログラム例を以下に示す。
=============================================
#include "stdafx.h"
#include "finetimer.h"
using namespace System;
using namespace mytimer;
int main(array<System::String ^> ^args)
{
const int N = 100;
int x[N];
int cnt = 0;
int t0, t1, ckt0, ckt1;
MyTimer
mt;
ckt0
= t0 = mt.timems();
do {
t1
= mt.timems();
if (t1 > t0){
x[cnt++]
= t1 - t0;
t0
= t1;
}
}
while (cnt < N);
ckt1
= t1;
for (int i = 0; i
< N; i++){
Console::Write((x[i].ToString()
+ "ms")->PadLeft(10));
if (i % 5 == 4) Console::WriteLine();
}
Console::WriteLine();
Console::WriteLine("Mean interval between increments = " +
((ckt1 - ckt0) / double(N)).ToString() + "ms");
for (int i = 0; i
< 10; i++){
int ta0 = GetTickCount();
int tb0 = mt.timems();
do {; } while
(GetTickCount() < ta0 + 50);
int ta1 = GetTickCount();
int tb1 = mt.timems();
Console::WriteLine(("ta1-ta0 = " +
(ta1-ta0).ToString())->PadLeft(15) + ("tb1-tb0
= " + (tb1-tb0).ToString())->PadLeft(15));
}
Console::WriteLine();
for (int i = 0; i
< 10; i++){
int ta0 = GetTickCount();
int tb0 = mt.timems();
do {; } while
(mt.timems() < tb0 + 50);
int ta1 = GetTickCount();
int tb1 = mt.timems();
Console::WriteLine(("ta1-ta0 = " + (ta1-ta0).ToString())->PadLeft(15)
+ ("tb1-tb0 = " +
(tb1-tb0).ToString())->PadLeft(15));
}
Console::WriteLine();
Console::WriteLine("Press the Enter key.");
Console::ReadLine();
return 0;
}
=============================================
上のプログラムを実行すると以下のようになる。
=============================================
1ms 1ms 1ms 1ms 1ms
1ms 1ms 1ms 1ms 1ms
1ms 1ms 1ms 1ms 1ms
1ms 1ms 1ms 1ms 1ms
1ms 1ms 1ms 1ms 1ms
1ms 1ms 1ms 1ms 1ms
1ms 1ms 1ms 1ms 1ms
1ms 1ms 1ms 1ms 1ms
1ms 1ms 1ms 1ms 1ms
1ms 1ms 1ms 1ms 1ms
1ms 1ms 1ms 1ms 1ms
1ms 1ms 1ms 1ms 1ms
1ms 1ms 1ms 1ms 1ms
1ms 1ms 1ms 1ms 1ms
1ms 1ms 1ms 1ms 1ms
1ms 1ms 1ms 1ms 1ms
1ms 1ms 1ms 1ms 1ms
1ms 1ms 1ms 1ms 1ms
1ms 1ms 1ms 1ms 1ms
1ms 1ms 1ms 1ms 1ms
Mean interval between increments = 1ms
ta1-ta0 = 62 tb1-tb0 = 59
ta1-ta0 = 63 tb1-tb0 = 63
ta1-ta0 = 62 tb1-tb0 = 62
ta1-ta0 = 63 tb1-tb0 = 62
ta1-ta0 = 62 tb1-tb0 = 62
ta1-ta0 = 62 tb1-tb0 = 62
ta1-ta0 = 63 tb1-tb0 = 63
ta1-ta0 = 62 tb1-tb0 = 62
ta1-ta0 = 63 tb1-tb0 = 62
ta1-ta0 = 62 tb1-tb0 = 62
ta1-ta0 = 47 tb1-tb0 = 50
ta1-ta0 = 47 tb1-tb0 = 50
ta1-ta0 = 46 tb1-tb0 = 50
ta1-ta0 = 47 tb1-tb0 = 50
ta1-ta0 = 63 tb1-tb0 = 50
ta1-ta0 = 46 tb1-tb0 = 50
ta1-ta0 = 47 tb1-tb0 = 50
ta1-ta0 = 47 tb1-tb0 = 50
ta1-ta0 = 47 tb1-tb0 = 50
ta1-ta0 = 62 tb1-tb0 = 50
Press the Enter key.
=============================================
上の計時用クラス型MyTimer使用例のプログラムの実行形式プログラムfinertimer.exeは、プログラム名finertimer.exeをクリックして実行することができる。ただし、.NET Framework3.5対応である。プログラムのソースコードファイルなどをまとめた圧縮ファイルfinertimer.zipは圧縮ファイル名finertimer.zipをクリックしてダウンロードしたものをマウスの右ボタンでクリックして表示されるメニュから「解凍」あるいは「展開」を選んで解凍することができる。解凍したファイルはVisual C++2008などで開くことができる。