昨日からOpenGL。面白い。
ベンチマークの仕方についてちょこっとメモ。
関数が呼び出されてから、その関数が終了するまでにかかる時間を計測したいとします。
単純に考えれば、関数の始まりのタイミングでその時のシステム時間を記録して、関数の終わりのタイミングでシステム時間を記録。それらの差をとって返す、っていうのが基本的な流れであることに変わりはありません。
C言語でこれらをやろうとすると、以下のようなコードになると思います。ちなみにマイクロ秒の単位で返しています。
#include <sys/time.h> #include <stdio.h> #include <stdlib.h> struct benchmark_counter { struct timeval starttime; struct timeval finishtime; }; struct benchmark_counter *BenchMarkStart(void) { struct benchmark_counter *counter = (struct benchmark_counter*) malloc(sizeof(struct benchmark_counter)); if (counter) gettimeofday(&(counter-> starttime), NULL); return counter; } void BenchMarkFinish(struct benchmark_counter *counter, char *function, char *output) { int time = 0; FILE *fp; if (counter) { gettimeofday(&(counter-> finishtime), NULL); time = (counter->finishtime.tv_sec - counter->starttime.tv_sec) * 1000 + (counter->finishtime.tv_usec - counter->starttime.tv_usec); free(counter); } else { time = 0; } fp = output != NULL? fopen(output, "a") : stderr; if (fp) { fprintf(fp, "%s\t%d\n", function, time); if (fp != stderr) fclose(fp); } } void function(void) { struct benchmark_counter *counter = BenchMarkStart(); sleep(5); BenchMarkFinish(counter, "function", NULL); } int main(void) { function(); return 0; }
struct timevalについては、詳しい解説がそのへんにゴロゴロあると思うので、そちらに任せるとして、要はBenchMarkStart()を計測したい関数の頭に挿入、関数が終わるところでBenchMarkFinish()を挿入して、指定したファイルに書き込んでいるわけです。
さて、C++でやるとどうなるか。もちろんこれと同じ方法でも計測は可能です。
ところで、上記の例だと、計測しているfoo()のreturnする場所は間違いなく一箇所しかないので、計測関数の挿入もシンプルでしたが、そうでない場合、特に、Cはすぐにネストが深くなってしまうので、エラーチェックをするごとにダメなら関数を終了させるようなコードを書いていると、BenchMarkFinish()を大量に書くことになってしまいます。
一方、C++の場合、クラスのコンストラクタ・デストラクタを使うとこの問題をずいぶん楽に解決できます。
これらの機能について賛否両論あるのはここでは置いておくとして、静的に関数内で生成されたクラスのインスタンスは自動変数と同じ扱いなので、その関数が終了するタイミングでそのインスタンスも廃棄され、そのタイミングでデストラクタがコールされます。
つまり、デストラクタコールのタイミングでかかった時間を計算、ファイルへの書き込みもしてしまおうというわけです。
以下、そのコード。ちなみに、上記のcのコードをもとに作ったものなので、iostreamとかそういう意味でのc++らしいコードにはなっていません(正確には、使えないだけですが)。
#include <sys/time.h> #include <stdio.h> #include <unistd.h> #define BENCHMARK_START(function,output) BenchMark ____benchmarkcounter(function,output); class BenchMark { private: struct timeval start; struct timeval end; int spendtime; FILE *fp; char *m_filename, *m_function; public: BenchMark(char *function, char *filename) { m_filename = filename; m_function = function; gettimeofday(&start, NULL); } ~BenchMark() { gettimeofday(&end, NULL); spendtime = (end.tv_sec - start.tv_sec) * 1000 * 1000 + (end.tv_usec - start.tv_usec); fp = m_filename != NULL ? fopen(m_filename, "a") : stderr ; if (fp) { fprintf(fp, "%s\t%d usec\n", m_function, spendtime); if (fp != stderr) fclose(fp); } } }; int function() { BENCHMARK_START((char*)"main", NULL); sleep(5); } int main(void) { function(); return 0; }
ちなみに、cの場合、システムコールとか、多少ヘッダのインクルードがちゃんとされていなくてもチェックが甘いのでコンパイル通ることありますが、c++の場合、きちんとunistd.hのインクルードが必要です。なお、上記はどちらも、出力先ファイル名にNULLを渡した場合は、stderrに出力するようになっています。
ちなみに、関数名を明示的に渡しているところがありますが、VC++だと、自動的にそのコンテクストの関数名に置き換えてくれる__FUNCDNAME__っていう予約マクロがあるんですがgcc系列はこう言うのないんでしょうか?__LINE__とか__DATE__はあるのに。
自分はどちらかと言えばcppはアンチですが、あの沢山の規則及び機能をうまく使えるのであれば、アリなんじゃないかと思います。まぁ、今から数十年たってもそうなれる気はしませんがw