パフォーマンスを計測するには、高精度なカウンターが必要である。このカウンターは、決まった周期で値が刻まれることにより、正確な経過時間の計測ができる。
最近、GNU/Linuxに移行したので、またどうもこの環境に慣れていない。ともかくGNU/Linuxで高精度なカウンターを使う方法を調べることにした。つまり、Win32 APIでいうところの、QueryPerformanceCounterのようなものがほしい。
まず見つかったのはPOSIXのclock_gettimeだ。これを使えば、ナノ秒単位での分解能が得られる。ただし、timespec構造体が非常にややこしい作りになっている。秒とナノ秒に分かれているのだ。これは面倒だ。こんなインターフェースでは間違えたコードを書いてしまいそうだ。
また、未だにGNU/Linux環境におけるライブラリのリンク方法がよくわからないのだが、どうもclock_gettimeを使うには、rtと呼ばれるライブラリとリンクしなければならないらしい。が、しかしそんなのはmanページには書いていない。さらに不思議なことに、そのオプションの渡し方が解せない。
gcc -lrt hoge.c
ではだめで、
gcc hoge.c -lrt
は動く。理由が謎だ。わけがわからない。GNU/Linux環境にはまともなドキュメントが不足している。
そこで、C++11の登場だ。C++11では、もっと簡単なライブラリが用意されている。しかもポータブルだ。ただ、現時点ではまともな資料が少ないので、使いにくいかもしれない。それに、ポータブルといっても、現状ではPOSIXの方が実装が多いように思う。
使い方としては、std::chrono::high_resolution_clock::now()を呼び出すだけだ。つまり、パフォーマンスを計測するのであれば、
auto time_point = std::chrono::high_resolution_clock::now() ; // ここにパフォーマンスを計測したい処理を書く auto duration = std::chrono::high_resolution_clock::now() - time_point ;
これだけでいい。問題は、ここから先だ。std::chrono::high_resolution_clock::now()が返すのは、time_pointクラスである。time_point同士を引き算すると、durationクラスになる。durationクラスのカウント数は、メンバー関数countを呼び出すことで得られる。
std::cout << duration.count() << std::endl ;
問題は、high_resolution_clockの精度が、規格では定められていないということだ。そのため、単にこのduration.count()の返す値は、このままでは一体何秒を意味するのかわからない。
そこで、このdurationの型が何であろうと関係ないポータブルなコードを書く必要がある。それには、duration_castが使える。
auto count = std::chrono::duration_cast< std::chrono::microseconds >( duration ).count() ;
このコードは、durationのテンプレート引数がなんであれ、マイクロ秒に変換する。したがって、countは、そのままマイクロ秒数として扱うことができる。ちなみに、他の単位も豊富に用意されている。必要とあれば、自分で単位を作り出すこともできる(例えば、5分単位の刻みだとか)
と、ここまで書いて思ったのだが、これを一体どうやって教育すればいいのだろうかということだ。QueryPerformanceCounterやclock_gettimeは、とても低級なCの関数である。引数に渡すのも、とても低級な構造体である。ポインターを使わなければならない。その結果は、あまりにも低級で生々しい値なので、実際に使用するためには、正しく加工しなければならない。すなわち、単位の変換だ。この加工作業は、単純ではあるものの、かなり難しい。私は変なことを書いているのではないし、世の中のプログラマーの能力をみくびっているわけでもない。多くのプログラマーは、こういう加工作業や、バージョン確認などといった、一見単純に思えるコードの記述を間違える。でなければ、ブラウザーやAdobe Flash Playerやドライバーのバージョンが上がっただけで動かなくなるソフトウェアがこんなにも多いはずがない。
しかし、Cプログラマーは、ポインターを正しく使えることが期待されているし、どんなに些細で単純なコードでも、自力で正しいコードを書くことが期待されている。したがって、これは問題にはならない。
その点から見ると、C++11のライブラリは非常に高級である。time_pointクラスは、ある起点からの経過カウント数であり、time_pointクラス同士を引き算した結果は、durationになる。これは当然だ。引き算した結果は、起点からのカウント数ではなく、単なるカウント数なので、durationになるのだ。ポインターも必要ないし、余計な加工もいらない。演算子のオーバーロードにより直感的に扱えるし、型同士の変換もduration_castが用意されている。
C++ユーザーは、C言語で必要な低級な能力を必要とされない。
問題は、time_pointやdurationが、任意の精度を取れるように、精度をテンプレート引数として指定可能なことだ。たとえば、gccのSTL実装では、std::chrono::high_resolution_clock::time_point、すなわちnow()の返す型は、
std::chrono::time_point<std::chrono::system_clock, std::chrono::duration<long, std::ratio<1l, 1000000l> > >
という、途方もない型になっている。何気ないコードで、こんな恐ろしい型が使われているのだ。
C++11では、auto指定子のおかげで、表面上はこのような恐ろしい型を目にせずともすむ。しかし、ユーザーが標準ライブラリを本当に使いこなそうとすると、やはりこういう詳細を知ることが重要になってくる。この詳細を理解するためには、テンプレートの詳細な理解が必須である。はたして、普通のユーザーにできるだろうか。これを考えると、ポインターとか生の値の加工とかが、可愛く見えてくるではないか。
一日中、C++規格を眺めて暮らしている私のような人間はいいとして、C++を単に道具として使いたいユーザーが、貴重な時間をこんな詳細の理解に割きたいと思うだろうか。
一般の用途ではGenericsがあれば十分な気がします。コンパイル時Raytracingとかコンパイル時高速フーリエ変換とかを楽しむ人のせいで物事が複雑になっている。
ReplyDeletePythonのように教育コストの面も考えてほしいです。
> さらに不思議なことに、そのオプションの渡し方が解せない。
ReplyDeletehttp://sourceware.org/binutils/docs-2.22/ld/Options.html#index-groups-of-archives-132
> ... Normally, an archive is searched only once in the order that it is
> specified on the command line. ...
リンクについては、ldのman pageを読むのがいいと思います。
ReplyDeleteなるほど。あとでじっくり読む必要がありますね。
ReplyDelete