JavaとC

SPECjvm98の_201_compressをC言語に移植して性能を測定した。移植したソースはここに載せるとマズいと思うので秘密。

Javaで書かれたプログラムをCに移植する際に色々とやり方はあると思うが、とりあえずJavaのクラスは構造体とした。compressはオブジェクト指向っぽい書き方はされていないので、Javaのメソッド呼び出しは全て静的な関数呼び出しとした。関数の第1引数はその構造体へのポインタ this である。こんな感じ。

static void
Compressor_cl_block(Compressor *this) {
    int rat;
    this->checkpoint = this->in_count + CHECK_GAP;
    if(this->in_count > 0x007fffff) {	/* shift will overflow */
        ...

当初、this.fieldという形のフィールドアクセスは全て this->field に書き換えていたが、これでは遅すぎる(特にgcc)ので適当なところ(ループの直前など)で this->field の値を変数にロードし、以降はその変数を参照するようにした。Cコンパイラが冗長なロードを除去してくれないので手動で最適化したわけである。さらに、compress中には小さなメソッドの呼び出しが頻出するがCコンパイラはこれをなかなかインライン展開してくれないので手動でほとんどを展開した。Javaのヌルチェックと範囲チェックはCバージョンでは行っていない。

測定環境はDELLのサーバマシンで、Intel Xeon 3.20GHz x2、主メモリは2GB、OSはLinux 2.4.25 である。実行時間は3回連続実行の最速値*1

コンパイラ/JVM オプション 実行時間(秒)
Intel icc 8.1 -O3 -xN -static 3.03
gcc 3.1 -O3 -mcpu=pentium4 -static 2.97
Sun HotSpot Server VM 1.4.2-b28 -server 4.63
Sun HotSpot Server VM 1.5.0-b64 -server 4.035
IBM Java DK 1.3.1 なし 3.772

gccが意外と頑張ってる。アセンブラを見てもすっきりした良いコードを吐いている。もっとも、手動で最適化したおかげでもあるんだけど。Cコンパイラにつけてる -xN とか -mcpu=pentium4 とか -static とかはほとんど効果は無いのだが、一応。icc で -fast を指定すると正しく実行時間が測れない。-fastを指定すると自動的に-ipo (inter-procedure optimization)がつくのだが、それがまずいようだ。

IBMJITが吐いたコードをデバッガの逆アセンブラで眺めたが、徹底的にインライン展開しており、冗長性除去もちゃんとしているのはさすが。gccと違って%ebpレジスタも汎用レジスタとして用いているので使用可能なレジスタの数が一つ多いと言える。ただし、gccよりmov命令の数が多い*2のでgccよりは確かに遅いだろう。JITコンパイルのオーバーヘッドもCより遅い原因の一つではあろうが、プロファイルによると計測区間内でJITコンパイルされるメソッドの数は20個も無い。しかもCPUが2個*3あるのでJITコンパイルが裏で行われている可能性もある*4。ヌルチェックは完全にimplicit化*5されている。範囲チェックはループ内に残っているが数が少ないので性能にほとんど影響はないだろう。

ちなみにヒープサイズに関するオプションを指定していないが、SunとIBMいずれも例えば -Xms1000m -Xmx1000m としても全く性能は変わらない。GC時間の合計は50m秒程度のようだ。

問題はライブラリで、compressは計測区間内で大きなファイル(数メガ)の読み込みを行う。JavaではI/Oストリーム、Cではfopen()/fread()/fclose()を用いているが、これに関しては公平な比較というのはそもそも無理なので仕方がない。プロファイルを取ってI/Oにどれくらい時間が取られているかを見るのが正しいのだが、今回はやってない。compressは確か全体の10%程度がI/Oの時間だったような気がする。

ちなみにIBMは現在、J9/TRというJVM/JITコンパイラを絶賛開発中。まだ一般公開されてないけど。いや、組み込み向けでは一応公開されてるか。(WebSphereに同梱されてるのは組み込み向け?)

*1:ちなみにcompressは数メガのファイルを5つ読み込むので、ファイルキャッシュに入っていない場合は最初の1回が遅くなる傾向にある。

*2:レジスタアロケータか何かのせいか?

*3:というかHT-enabledだったような気がするので4個か?

*4:確かIBMJITは一番強い最適化をかけるときのみ別スレッドで動くんだっけ?

*5:load/store命令におけるSIGSEGVを利用。