ここで言うコード(⇔データ)のパラメーター化とは、仮想関数や関数へのポインター等を使用して、関数内の一部の挙動を関数外部から与えられるようにすることである。例えば下記の関数は、10回繰り返すという機能を提供している。どのような処理を繰り返すのかは、引数jobで与える。
void repeat10Times(void (*job)()) { for (int i = 0; i < 10; ++i) { job(); } }
C++11では、コードをパラメーター化する方法は複数存在するが、どんなときにどの方法が適しているか整理されている記述を見かけたことがないので、整理してみることにした。コードのチューニングをしている際に、どの方法が高速なのか知りたいというのが元々の動機だったので、速度も比較した。
パラメーター化の方法としては、以下を対象とした。
関数にラムダ式を渡す方法として、テンプレートのtypenameパラメータを使う方法と、std::functionを使う方法の2パターンを試したので、今回は、合計5つのパラメーター化の方法を比較した。各方法のプログラムは、以下のようになる。
gcc 4.8.1で、「g++ -std=c++11 -O3 -Ofast」でコンパイルし、実行速度を比較した結果は下記の通り。各実装を100回「RunLoop(false, 10000000)」のように呼び出した際の実行時間だ。
前述のコードでは、各実装が一つのコンパイル単位に記述されているかのように見えるが、実際の測定に使用したプログラムでは、複数のコンパイル単位に分割している(ただし、テンプレート系は、インクルードするので一つのコンパイル単位になる)。例えば、FuncPtr::LoopとFuncPtr::RunLoopは異なるコンパイル単位にしている。そうしないと、最適化によりFuncPtr::Loop内にFuncPtr::EmptyJobがインライン展開される(更に最適化されて、FuncPtr::Loop内のforループ自体が削除される)。
一度しか測定していないので、処理時間のばらつきの関係で上段より下段の方が速いケースもある。しかし、何度も測定し最善値を比べれば、上段より下段の方が速いなんてことはないはず。
やはり、コンパイル時に全てが確定し、コンパイラが最適化しやすいテンプレート系(ラムダ式(テンプレート)も含む)が、一番速い。関数へのポインターと残り2つとの差はわずかである。
同じテストを、別のマシン(MacBook Pro)で行った。コンパイラは、Xcode 4.6のApple LLVM compiler 4.2で最適化は、Fastest[-O3]を指定した。
さすがLLVMコンパイラー。NOP(i)を単純な加算にしたくらいでは、ループするコードを生成しないようだ。おそらく加算をループで回す代わりに、掛け算(と割り算かシフト)で計算しているのだろう。
実行速度が分かったところで、コードのパラメーター化の各方法の評価をまとめてみた。 評価の観点によってどの方法が優れているかが異なり、 一概にどの方法が優れているとは言えない。どの観点を重視するかに応じて使い分けるのが良さそう。