PerformanceCounterを使ったマイクロ秒時計

PerformanceCounterを使ったマイクロ秒時計

Linuxには、gettimeofdayがありますが、
Windowsには、マイクロ秒まで計測可能なAPIは用意されていません。

PerformanceCounterを使えばかなり正確な時間間隔を知ることはできますが、
時計として利用することは簡単ではありません。
ある程度観測をしていると、ansi標準の_ftimeとはズレていきます。
このため、_ftimeをベースとしてミリ秒以下の部分をPerformanceCounterから得ることを考えましたが、秒の変わり目で時計が逆に進む場合があります。
そこで、ftimeとPerformanceCounterから得られる時間差を調節し、
時計が逆に進むことがないようなマイクロ秒時計を作ってみました。

usec.h

// (c) Hundredsoft Corporation 2010 All rights reserved.

#ifndef USEC_INCLUDED
#define USEC_INCLUDED
class usec
{
public:
	usec();
	~usec(){}
	__int64 get();
private:
	__int64 microfunc(__int64 a, __int64 b);
	__int64 usec::getstm();
	LARGE_INTEGER m_ti;
	LARGE_INTEGER m_tfi;
	__int64 m_msi;
	__int64 m_lastus;
	bool m_isSuported;
};
#endif



usec.cpp

// (c) Hundredsoft Corporation 2010 All rights reserved.

#include <windows.h>
#include <time.h>
#include <sys/timeb.h>
#include "usec.h"

usec::usec()
{
	m_isSuported = false;
	if (QueryPerformanceFrequency(&m_tfi)){
		if (QueryPerformanceCounter(&m_ti)){
			m_isSuported = true;
		}
	}
	m_msi = getstm();
}

__int64 usec::microfunc(__int64 a, __int64 b)
{
	__int64 ah, al, a0, m = 0;
	int i;
	ah = (a >> 44) & 0xffff;
	al = a << 20;
	for (i=0; i<128; i++){
		m <<= 1;
		if (ah < 0) m |= 1;
		ah <<= 1;
		if (al < 0) ah |= 1;
		al <<= 1;
		if (m >= b){
			m -= b;
			al |= 1;
		}
	}
	a0 = al;
	ah = (a0 >> 44) & 0xffff;
	al = a0 << 20;
	m = 0;
	for (i=0; i<128; i++){
		m <<= 1;
		if (ah < 0) m |= 1;
		ah <<= 1;
		if (al < 0) ah |= 1;
		al <<= 1;
		if (m >= 22634874){
			m -= 22634874;
			al |= 1;
		}
	}
	return a0 - al;
}

__int64 usec::getstm()
{
	_timeb  tb;
	_ftime(&tb);
	struct tm* stm = localtime(&tb.time);
	__int64 stms = (stm->tm_hour * 3600000 + stm->tm_min * 60000 + stm->tm_sec * 1000 + tb.millitm);
	return stms * 1000;
}

__int64 usec::get()
{
	__int64 utm;
	if (m_isSuported){
		LARGE_INTEGER t, tf;
		QueryPerformanceFrequency(&tf);
		QueryPerformanceCounter(&t);
		__int64 stms = getstm();
		__int64 diff = t.QuadPart-m_ti.QuadPart;
		if (diff > 0){
			utm = m_msi + microfunc(diff, tf.QuadPart);
			diff = utm - stms;
		}else{
			utm = stms;
			diff = 999999999;
		}
		__int64 absdiff = (diff < 0) ? -diff : diff;
		if (m_tfi.QuadPart != tf.QuadPart || absdiff > 10000){
			m_tfi = tf;
			m_ti = t;
			m_msi = stms;
			if (diff > 0){
				if (utm - m_lastus > 10 )
					utm = (m_lastus + utm) / 2;
				else
					utm = m_lastus + 2;
			}else{
				if (absdiff > 3600000000)
					utm = m_msi;
				else
					utm = (utm + m_msi) / 2;
			}
		}
		m_lastus = utm;
	}else{
		utm = getstm();
	}
	return utm;
}





テスト用コード
main.cpp

#include <tchar.h>
#include <windows.h>
#include <stdio.h>
#include "usec.h"

#include <time.h>
#include <sys/timeb.h>

int main(int argc, char**argv)
{
	usec ut;
	while(1){
		char work[256];
		__int64 utm = ut.get();

		_timeb  tb;
		_ftime(&tb);
		struct tm* stm = localtime(&tb.time);

		sprintf(work, "%02d:%02d:%02d.%06d [%02d.%03d]",
				(int)((utm / 3600000000) % 24),
				(int)((utm / 60000000) % 60),
				(int)((utm / 1000000) % 60),
				(int)(utm % 1000000),
				stm->tm_sec,
				tb.millitm
				);
		printf("%s\n", work);
		Sleep(300);
	}
}
usec::microfuncは、PerformanceCounterから得られる値(LARGE_INTEGER)から
マイクロ秒を得る128bit除算を行っています。

ある時間差(t0〜t1)があったとして
t0,t1はQueryPerformanceCounterによって得られる値とします。
t = t1 - t0、この時間間隔は、周波数:f(QueryPerformanceFrequencyによって得られる)として
t / f で得られますが、
整数演算でマイクロ秒を得たいので
t * 1000000 / f
となります。
このままdoubleで演算しても良いのですが整数演算に拘って、
t * (1024*1024 - 48576) / f
= 1024*1024 * t / f * (1 - 1 / 21.58629776021)
= 1024*1024 * t / f * (1 - 1024*1024 / 22634874)
A = (t << 20) / f
と置けば
≒A - (A << 20) / 22634874 [μSec]となります。
これは、64bitを超える演算になりますのでforで回して計算しています。

テスト用コードでは、usecクラスから得られたマイクロ秒時間と共に、
_ftimeによる「秒、ミリ秒」も表示しています。

usec::getでは、ftimeとPerformanceCounterから得られる時間差を調節しているので、
それほど精度の高い時計ではありませんが、ミリ秒単位の時計だとループ間隔を短くSleep(10)とかにすると、同じ時間が続いてしまいます。
この時計の場合は、ループ間隔(Sleep)を短くしても、同じ時間が並ぶようなことはありません。

※高分解能パフォーマンスカウンタがないPCでは、ミリ秒単位の出力となります。



usec.zipソースはこちら



Tags: プログラムメモ
author : HUNDREDSOFT | - | -