経過時間の測定方法
超高性能プログラミング技術のメモ(5)
高性能プログラミングで必須の、経過時間測定の方法について書いておきます。
時間を測定する関数
高性能プログラミングでは、処理が高速になっていくため、時間測定の解像度も高くないといけません。プログラミング言語に用意されているタイマー関数は解像度が低いため、システムコールを使う必要があります。近年のLinuxでは、下記のような関数を使うことができます。
ナノ秒単位で測定できるclock_gettime()関数があれば良いですが、システムによっては存在しない場合もあります。また、時間を受け取るデータ型も各関数で異なります。そのため、システム毎に使える関数をチェックし、測定コードを挟み込むのは非常に手間のかかる割に生産性のない作業になります。
上記の図には、CPU時間とWall時間が分けて書いてありますが、違いについて簡単に説明します。CPU時間は、CPUを使用した時間です。Wall時間は、CPUを使用したかどうかに関わらず、過ぎ去った時間のことです。
超高性能プログラミングの時間測定では、実際の時間を測定するWall時間の方を使います。
時間測定関数をラッピング
システム毎に使用する時間測定関数を書き換えるのは非常に無駄なので、私は自作ライブラリを用意しています。そのライブラリでは、get_cputime(), get_realtime(), get_tick()という3つの関数を用意しています。関数の実装を、システムに合わせて、configureを使って切り替えを行っています。戻り値は、倍精度浮動小数点数で統一しています。
下のコードは、clock関数とtimes関数を使った実装です。この実装のメリットは、ほとんどのUnixシステムで使用できることです。反対にデメリットは、解像度が1e-2のため、1ミリ秒以下の差は測定することができないことです。そのため、高性能プログラミングでは、ほとんど使い物になりません。
double get_cputime(void)
{
clock_t t=clock();
return (double)t/(double)CLOCKS_PER_SEC;
}
double get_realtime(void)
{
struct tms m;
clock_t t=times(&m);
return (double)t/(double)sysconf(_SC_CLK_TCK);
}
double get_tick(void){ return (double)1e-2; }
次のコードは、システム用の関数を使った実装です。CPU時間は、リソース利用時間を取り出すgetrusageを使います。rusage構造体には、ru_utime(ユーザ時間)とru_stime(システム時間)が含まれています。ユーザ時間はユーザプログラムが使用した時間、システム時間はシステムコールが使用した時間です。ここで測定したいのはユーザプログラムの時間なので、ru_utimeを使っています。
Wall時間には、その日の時刻を取得するgettimeofdayを使用します。戻り型は、timeval構造体です。timeval構造体は、tv_sec(秒)とtv_usec(マイクロ秒)の時刻を持っているので、これを倍精度実数に換算して返します。
/* micro sec */
double get_cputime(void)
{
struct rusage r;
struct timeval t;
getrusage(RUSAGE_SELF,&r);
t = r.ru_utime; // user time
//t = r.ru_stime; // system time
return t.tv_sec + (double)t.tv_usec*1e-6;
}
double get_realtime(void)
{
struct timeval t;
gettimeofday(&t,NULL);
return t.tv_sec + (double)t.tv_usec*1e-6;
}
double get_tick(void){ return (double)1e-6; }
下のコードは、Linux 2.6以降のシステムコールであるclock_gettime関数を使用しています。ナノ秒の解像度を持ち、非常に短い時間をも測定できることがメリットです。しかし、古いLinuxやUnixシステムでは使うことができません。
clock_gettime関数は、第1引数によって取得する時刻の種類を変更します。CLOCK_PROCESS_CPUTIME_IDは、プロセス単位のCPUクロックで、そのプロセスの全スレッドで消費されるCPU時間を返します。よく似たCLOCK_THREAD_CPUTIME_IDは、スレッド固有のCPUクロックを返します。CLOCK_REALTIMEは、システム全体で一意な実時間を返します。
/* nano sec */
double get_cputime(void)
{
struct timespec t;
clock_gettime(CLOCK_PROCESS_CPUTIME_ID,&t);
//clock_gettime(CLOCK_THREAD_CPUTIME_ID,&t);
return t.tv_sec + (double)t.tv_nsec*1e-9;
}
double get_realtime(void)
{
struct timespec t;
clock_gettime(CLOCK_REALTIME,&t);
return t.tv_sec + (double)t.tv_nsec*1e-9;
}
double get_tick(void){ return (double)1e-9; }
これらの計測関数は、次のようにして使用します。
double t1, t2;
t1 = get_realtime();
do_function();
t2 = get_realtime();
printf("do_function %G s¥n",t2-t1);
まとめ
今回は、高性能化を確かめるために必要な、時間計測について書きました。今回の関数をライブラリ化したコードは下記にありますので、興味があればダウンロードしてみて下さい。
ビルド方法は、./configureとmakeです。同梱のTimer.hをincludeし、生成されたlibtimer.aをリンクして利用して下さい。