gccのprofiled optimization

CPUをアーキテクチャ指定による最適化の具合を調べていたんだけど、最近のgcc*1にはprofiled optimizationの機能が入っていることに気付いたのでこちらを測定してみた。
測定に使ったベンチマークSciMark2.0ANSI C版。コンパイラDebian sid/amd64gcc version 4.2.1 20070516 (prerelease) (Debian 4.2-20070516-1)。
通常にコンパイルしたバイナリ

**                                                              **
** SciMark2 Numeric Benchmark, see http://math.nist.gov/scimark **
** for details. (Results can be submitted to pozo@nist.gov)     **
**                                                              **
Using       2.00 seconds min time per kenel.
Composite Score:          501.16
FFT             Mflops:   324.21    (N=1024)
SOR             Mflops:   426.32    (100 x 100)
MonteCarlo:     Mflops:   176.60
Sparse matmult  Mflops:   777.88    (N=1000, nz=5000)
LU              Mflops:   800.78    (M=100, N=100)

実行プロファイルを喰わせて作ったバイナリ

**                                                              **
** SciMark2 Numeric Benchmark, see http://math.nist.gov/scimark **
** for details. (Results can be submitted to pozo@nist.gov)     **
**                                                              **
Using       2.00 seconds min time per kenel.
Composite Score:          633.20
FFT             Mflops:   436.06    (N=1024)
SOR             Mflops:   454.47    (100 x 100)
MonteCarlo:     Mflops:   177.19
Sparse matmult  Mflops:   926.30    (N=1000, nz=5000)
LU              Mflops:  1171.96    (M=100, N=100)

相手が内容固定の数値計算というプロファイル最適化が最も効きそうな素材ではあるが、25%ぐらい速くなった。こりゃ凄い。
実行プロファイルの作り方は、まず実行プロファイルを出力する専用バイナリを作ることから始まる。これは、コンパイル時とリンク時に"-fprofile-generate"というオプションを付けてmakeすると出来る。このバイナリを実行すると.o毎に.gcdaや.gcnoという実行時の統計情報を書き出してくれる。あとは再度"-fprofile-use"というオプションに変えてmakeし直すと先に出力された統計情報に基いて最適化を行ったバイナリが作成される。
今回はどれぐらい効果があるか調べたいだけだったので、単純なシェルスクリプトを書いてバイナリを作った。

#!/bin/sh

make clean
rm *.gcda
rm *.gcno
make CFLAGS="-O3 -fprofile-generate" LDFLAGS="-fprofile-generate" CC=gcc-4.2

./scimark2
./scimark2

make clean
make CFLAGS="-O3 -fprofile-use" LDFLAGS="-fprofile-arcs"  CC=gcc-4.2

ちなみに、SciMarkにはキャッシュに入り切る通常のサイズの計算と主記憶にアクセスする必要がある程度の大きさの2通りの測定が出来る。上記の結果は小さい方の結果で、ほぼプロセッサのキャッシュで演算内容が完結するぐらい。この小さい方の演算のみで最適化を行ったバイナリに対してデータフローが異なるであろうラージモードの演算をやらせると、通常の最適化の結果と1%程度速いところまで差が縮まった。遅くなるようなことはないようだが、演算内容に特化した最適化が行われるのは確かなようだ。
詳しい動作原理などはRadium SoftwareProfile feedback in GCCという記事に詳しい。同じくmanにもある通り、値の内容に応じた最適化(演算結果を変えない範囲でシフト演算などより高速な処理にコンパイルする)やループの展開(事前に繰り返し回数を決定できるようなループを単純な列挙に展開する)、分岐の結果頻度に応じた最適化などを行う。
実行時プロファイルによる最適化は最近のJVMではわりと使われる手法のようで、JDK1.5ぐらいから入っていたように記憶している。起動時の遅さはともかくとして、実行時間が十分に長いアプリケーションでは「C言語で記述したものよりも速い」ということが起こり得る。実際、SciMark2.0のページにはJavaでの実行結果とそのプラットフォームがランキングで紹介されているが、上記結果を取ったPCとほぼ同等のCPU性能のマシンだと若干良い結果を出しているようである。
Visual C++コンパイラなどではかなり以前から備わっていた機能ではあるが、せっかく最近のgccを使うならばこういった機能を使ってみるのも良いかもしれない。

*1:具体的にはgcc-4.1以降